minimindslab-csv-sniff 0.1.0__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.
- minimindslab_csv_sniff-0.1.0/LICENSE +21 -0
- minimindslab_csv_sniff-0.1.0/PKG-INFO +53 -0
- minimindslab_csv_sniff-0.1.0/README.md +42 -0
- minimindslab_csv_sniff-0.1.0/pyproject.toml +19 -0
- minimindslab_csv_sniff-0.1.0/setup.cfg +4 -0
- minimindslab_csv_sniff-0.1.0/src/minimindslab_csv_sniff/__init__.py +103 -0
- minimindslab_csv_sniff-0.1.0/src/minimindslab_csv_sniff.egg-info/PKG-INFO +53 -0
- minimindslab_csv_sniff-0.1.0/src/minimindslab_csv_sniff.egg-info/SOURCES.txt +9 -0
- minimindslab_csv_sniff-0.1.0/src/minimindslab_csv_sniff.egg-info/dependency_links.txt +1 -0
- minimindslab_csv_sniff-0.1.0/src/minimindslab_csv_sniff.egg-info/top_level.txt +1 -0
- minimindslab_csv_sniff-0.1.0/tests/test_csv_sniff.py +37 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MiniMindsLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minimindslab-csv-sniff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Infer a CSV dialect from a short text sample and normalize it into a structured, testable description (delimiter, quote character, escape character, line terminator style, and header-likelihood).
|
|
5
|
+
Author: MiniMindsLab
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# MiniMindsLab CSV Sniff
|
|
13
|
+
|
|
14
|
+
MiniMindsLab CSV Sniff is a tiny, offline helper for inferring CSV dialect details from short text samples.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Infers delimiter (comma, tab, semicolon, pipe, or fallback)
|
|
19
|
+
- Reports quote character and escape character assumptions
|
|
20
|
+
- Classifies line terminator style (`lf`, `crlf`, `cr`, or `mixed`)
|
|
21
|
+
- Estimates whether a header row is likely
|
|
22
|
+
- Deterministic output suitable for tests and serialization
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from minimindslab_csv_sniff import sniff_csv
|
|
28
|
+
|
|
29
|
+
sample = "name,age\nAlice,30\nBob,41\n"
|
|
30
|
+
result = sniff_csv(sample)
|
|
31
|
+
print(result)
|
|
32
|
+
# {
|
|
33
|
+
# 'delimiter': ',',
|
|
34
|
+
# 'quotechar': '"',
|
|
35
|
+
# 'escapechar': None,
|
|
36
|
+
# 'lineterminator': 'lf',
|
|
37
|
+
# 'header_likely': True
|
|
38
|
+
# }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### `sniff_csv(sample: str) -> dict`
|
|
44
|
+
|
|
45
|
+
Raises `ValueError` when input is empty or whitespace-only.
|
|
46
|
+
|
|
47
|
+
Returns a dictionary with keys:
|
|
48
|
+
|
|
49
|
+
- `delimiter`
|
|
50
|
+
- `quotechar`
|
|
51
|
+
- `escapechar`
|
|
52
|
+
- `lineterminator`
|
|
53
|
+
- `header_likely`
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# MiniMindsLab CSV Sniff
|
|
2
|
+
|
|
3
|
+
MiniMindsLab CSV Sniff is a tiny, offline helper for inferring CSV dialect details from short text samples.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Infers delimiter (comma, tab, semicolon, pipe, or fallback)
|
|
8
|
+
- Reports quote character and escape character assumptions
|
|
9
|
+
- Classifies line terminator style (`lf`, `crlf`, `cr`, or `mixed`)
|
|
10
|
+
- Estimates whether a header row is likely
|
|
11
|
+
- Deterministic output suitable for tests and serialization
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from minimindslab_csv_sniff import sniff_csv
|
|
17
|
+
|
|
18
|
+
sample = "name,age\nAlice,30\nBob,41\n"
|
|
19
|
+
result = sniff_csv(sample)
|
|
20
|
+
print(result)
|
|
21
|
+
# {
|
|
22
|
+
# 'delimiter': ',',
|
|
23
|
+
# 'quotechar': '"',
|
|
24
|
+
# 'escapechar': None,
|
|
25
|
+
# 'lineterminator': 'lf',
|
|
26
|
+
# 'header_likely': True
|
|
27
|
+
# }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `sniff_csv(sample: str) -> dict`
|
|
33
|
+
|
|
34
|
+
Raises `ValueError` when input is empty or whitespace-only.
|
|
35
|
+
|
|
36
|
+
Returns a dictionary with keys:
|
|
37
|
+
|
|
38
|
+
- `delimiter`
|
|
39
|
+
- `quotechar`
|
|
40
|
+
- `escapechar`
|
|
41
|
+
- `lineterminator`
|
|
42
|
+
- `header_likely`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "minimindslab-csv-sniff"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Infer a CSV dialect from a short text sample and normalize it into a structured, testable description (delimiter, quote character, escape character, line terminator style, and header-likelihood)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "MiniMindsLab" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
package-dir = {"" = "src"}
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""MiniMindsLab CSV Sniff."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
__all__ = ["sniff_csv"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _line_terminator_style(sample: str) -> str:
|
|
13
|
+
has_crlf = "\r\n" in sample
|
|
14
|
+
cr_only_count = sample.count("\r") - sample.count("\r\n")
|
|
15
|
+
has_cr_only = cr_only_count > 0
|
|
16
|
+
has_lf = sample.count("\n") > sample.count("\r\n") or ("\n" in sample and not has_crlf)
|
|
17
|
+
|
|
18
|
+
kinds = sum([1 if has_crlf else 0, 1 if has_cr_only else 0, 1 if has_lf else 0])
|
|
19
|
+
if kinds > 1:
|
|
20
|
+
return "mixed"
|
|
21
|
+
if has_crlf:
|
|
22
|
+
return "crlf"
|
|
23
|
+
if has_cr_only:
|
|
24
|
+
return "cr"
|
|
25
|
+
return "lf"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _header_likely(lines: list[list[str]]) -> bool:
|
|
29
|
+
if len(lines) < 2 or not lines[0]:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
first = lines[0]
|
|
33
|
+
rest = lines[1:]
|
|
34
|
+
|
|
35
|
+
def is_number(text: str) -> bool:
|
|
36
|
+
t = text.strip()
|
|
37
|
+
if not t:
|
|
38
|
+
return False
|
|
39
|
+
try:
|
|
40
|
+
float(t)
|
|
41
|
+
return True
|
|
42
|
+
except ValueError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
score = 0
|
|
46
|
+
for col in range(len(first)):
|
|
47
|
+
head = first[col].strip()
|
|
48
|
+
if not head:
|
|
49
|
+
continue
|
|
50
|
+
head_is_num = is_number(head)
|
|
51
|
+
|
|
52
|
+
numeric_below = 0
|
|
53
|
+
nonempty_below = 0
|
|
54
|
+
for row in rest:
|
|
55
|
+
if col >= len(row):
|
|
56
|
+
continue
|
|
57
|
+
v = row[col].strip()
|
|
58
|
+
if not v:
|
|
59
|
+
continue
|
|
60
|
+
nonempty_below += 1
|
|
61
|
+
if is_number(v):
|
|
62
|
+
numeric_below += 1
|
|
63
|
+
|
|
64
|
+
if nonempty_below == 0:
|
|
65
|
+
continue
|
|
66
|
+
if (not head_is_num) and (numeric_below >= max(1, nonempty_below // 2)):
|
|
67
|
+
score += 1
|
|
68
|
+
|
|
69
|
+
return score >= max(1, len(first) // 3)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sniff_csv(sample: str) -> dict[str, Any]:
|
|
73
|
+
"""Infer CSV formatting details from a short sample.
|
|
74
|
+
|
|
75
|
+
Returns keys: delimiter, quotechar, escapechar, lineterminator, header_likely.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(sample, str):
|
|
78
|
+
raise ValueError("sample must be a non-empty string")
|
|
79
|
+
if sample.strip() == "":
|
|
80
|
+
raise ValueError("sample must not be empty or whitespace-only")
|
|
81
|
+
|
|
82
|
+
sniffer = csv.Sniffer()
|
|
83
|
+
candidates = [",", "\t", ";", "|", ":"]
|
|
84
|
+
try:
|
|
85
|
+
dialect = sniffer.sniff(sample, delimiters="".join(candidates))
|
|
86
|
+
delimiter = dialect.delimiter
|
|
87
|
+
quotechar = dialect.quotechar or '"'
|
|
88
|
+
escapechar = dialect.escapechar
|
|
89
|
+
except csv.Error:
|
|
90
|
+
delimiter = ","
|
|
91
|
+
quotechar = '"'
|
|
92
|
+
escapechar = None
|
|
93
|
+
|
|
94
|
+
reader = csv.reader(StringIO(sample), delimiter=delimiter, quotechar=quotechar)
|
|
95
|
+
rows = [row for row in reader if row]
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"delimiter": delimiter,
|
|
99
|
+
"quotechar": quotechar,
|
|
100
|
+
"escapechar": escapechar,
|
|
101
|
+
"lineterminator": _line_terminator_style(sample),
|
|
102
|
+
"header_likely": _header_likely(rows),
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minimindslab-csv-sniff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Infer a CSV dialect from a short text sample and normalize it into a structured, testable description (delimiter, quote character, escape character, line terminator style, and header-likelihood).
|
|
5
|
+
Author: MiniMindsLab
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# MiniMindsLab CSV Sniff
|
|
13
|
+
|
|
14
|
+
MiniMindsLab CSV Sniff is a tiny, offline helper for inferring CSV dialect details from short text samples.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Infers delimiter (comma, tab, semicolon, pipe, or fallback)
|
|
19
|
+
- Reports quote character and escape character assumptions
|
|
20
|
+
- Classifies line terminator style (`lf`, `crlf`, `cr`, or `mixed`)
|
|
21
|
+
- Estimates whether a header row is likely
|
|
22
|
+
- Deterministic output suitable for tests and serialization
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from minimindslab_csv_sniff import sniff_csv
|
|
28
|
+
|
|
29
|
+
sample = "name,age\nAlice,30\nBob,41\n"
|
|
30
|
+
result = sniff_csv(sample)
|
|
31
|
+
print(result)
|
|
32
|
+
# {
|
|
33
|
+
# 'delimiter': ',',
|
|
34
|
+
# 'quotechar': '"',
|
|
35
|
+
# 'escapechar': None,
|
|
36
|
+
# 'lineterminator': 'lf',
|
|
37
|
+
# 'header_likely': True
|
|
38
|
+
# }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### `sniff_csv(sample: str) -> dict`
|
|
44
|
+
|
|
45
|
+
Raises `ValueError` when input is empty or whitespace-only.
|
|
46
|
+
|
|
47
|
+
Returns a dictionary with keys:
|
|
48
|
+
|
|
49
|
+
- `delimiter`
|
|
50
|
+
- `quotechar`
|
|
51
|
+
- `escapechar`
|
|
52
|
+
- `lineterminator`
|
|
53
|
+
- `header_likely`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/minimindslab_csv_sniff/__init__.py
|
|
5
|
+
src/minimindslab_csv_sniff.egg-info/PKG-INFO
|
|
6
|
+
src/minimindslab_csv_sniff.egg-info/SOURCES.txt
|
|
7
|
+
src/minimindslab_csv_sniff.egg-info/dependency_links.txt
|
|
8
|
+
src/minimindslab_csv_sniff.egg-info/top_level.txt
|
|
9
|
+
tests/test_csv_sniff.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
minimindslab_csv_sniff
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from minimindslab_csv_sniff import sniff_csv
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestCsvSniff(unittest.TestCase):
|
|
7
|
+
def test_comma_with_header(self):
|
|
8
|
+
sample = "name,age\nAlice,30\nBob,41\n"
|
|
9
|
+
result = sniff_csv(sample)
|
|
10
|
+
self.assertEqual(result["delimiter"], ",")
|
|
11
|
+
self.assertEqual(result["quotechar"], '"')
|
|
12
|
+
self.assertTrue(result["header_likely"])
|
|
13
|
+
|
|
14
|
+
def test_tab_separated(self):
|
|
15
|
+
sample = "name\tage\nAlice\t30\nBob\t41\n"
|
|
16
|
+
result = sniff_csv(sample)
|
|
17
|
+
self.assertEqual(result["delimiter"], "\t")
|
|
18
|
+
|
|
19
|
+
def test_empty_or_whitespace_raises(self):
|
|
20
|
+
with self.assertRaisesRegex(ValueError, "empty or whitespace-only"):
|
|
21
|
+
sniff_csv("")
|
|
22
|
+
with self.assertRaisesRegex(ValueError, "empty or whitespace-only"):
|
|
23
|
+
sniff_csv(" \n\t")
|
|
24
|
+
|
|
25
|
+
def test_deterministic_output(self):
|
|
26
|
+
sample = "city;country\nParis;FR\nRome;IT\n"
|
|
27
|
+
a = sniff_csv(sample)
|
|
28
|
+
b = sniff_csv(sample)
|
|
29
|
+
self.assertEqual(a, b)
|
|
30
|
+
|
|
31
|
+
def test_non_string_input_raises(self):
|
|
32
|
+
with self.assertRaisesRegex(ValueError, "non-empty string"):
|
|
33
|
+
sniff_csv(None) # type: ignore[arg-type]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
unittest.main()
|