schemabind 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.
- schemabind-0.1.0/PKG-INFO +95 -0
- schemabind-0.1.0/README.md +77 -0
- schemabind-0.1.0/pyproject.toml +30 -0
- schemabind-0.1.0/schemabind/__init__.py +1 -0
- schemabind-0.1.0/schemabind/core.py +45 -0
- schemabind-0.1.0/schemabind.egg-info/PKG-INFO +95 -0
- schemabind-0.1.0/schemabind.egg-info/SOURCES.txt +9 -0
- schemabind-0.1.0/schemabind.egg-info/dependency_links.txt +1 -0
- schemabind-0.1.0/schemabind.egg-info/top_level.txt +1 -0
- schemabind-0.1.0/setup.cfg +4 -0
- schemabind-0.1.0/tests/test_core.py +101 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schemabind
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Expose existing dictionary fields as readable Python attributes.
|
|
5
|
+
Author: Adam
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/adamhx2/SchemaBind
|
|
8
|
+
Project-URL: Issues, https://github.com/adamhx2/SchemaBind/issues
|
|
9
|
+
Keywords: dictionary,attributes,csv,schema,developer-tools
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# SchemaBind
|
|
20
|
+
|
|
21
|
+
SchemaBind reduces repetitive dictionary lookup boilerplate by exposing existing fields as readable Python attributes.
|
|
22
|
+
|
|
23
|
+
Instead of:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
first_name = row["first_name"]
|
|
27
|
+
email = row["email_address"]
|
|
28
|
+
phone = row.get("phone_number")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from schemabind import bind
|
|
35
|
+
|
|
36
|
+
customer = bind(row)
|
|
37
|
+
|
|
38
|
+
customer.first_name
|
|
39
|
+
customer.email_address
|
|
40
|
+
customer.phone_number
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
SchemaBind is currently focused on dictionaries, including rows produced by `csv.DictReader`.
|
|
44
|
+
|
|
45
|
+
It does not create schemas, validate data, transform values, or infer new fields. It simply provides a cleaner way to access fields that already exist.
|
|
46
|
+
|
|
47
|
+
## Example
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import csv
|
|
51
|
+
from schemabind import bind
|
|
52
|
+
|
|
53
|
+
with open("samples/csv/customer.csv") as f:
|
|
54
|
+
rows = list(csv.DictReader(f))
|
|
55
|
+
|
|
56
|
+
customer = bind(rows[0])
|
|
57
|
+
|
|
58
|
+
print(customer.first_name)
|
|
59
|
+
print(customer.email_address)
|
|
60
|
+
print(customer.phone_number)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Missing attributes raise `AttributeError` instead of silently returning `None`, which helps catch typos in field names.
|
|
64
|
+
|
|
65
|
+
## Field Normalization
|
|
66
|
+
|
|
67
|
+
SchemaBind supports simple field-name normalization for attribute access.
|
|
68
|
+
|
|
69
|
+
For example:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
customer = bind({"First Name": "Kaladin"})
|
|
73
|
+
|
|
74
|
+
print(customer.first_name)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The original dictionary keys are preserved for dictionary-style helpers:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
customer.keys()
|
|
81
|
+
customer.get("First Name")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If multiple fields normalize to the same attribute name, SchemaBind raises `ValueError` instead of guessing which field to use.
|
|
85
|
+
|
|
86
|
+
For example:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
bind({
|
|
90
|
+
"First Name": "Dalinar",
|
|
91
|
+
"first_name": "Shallan",
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Both fields would normalize to `first_name`, so the binding is rejected as ambiguous.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# SchemaBind
|
|
2
|
+
|
|
3
|
+
SchemaBind reduces repetitive dictionary lookup boilerplate by exposing existing fields as readable Python attributes.
|
|
4
|
+
|
|
5
|
+
Instead of:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
first_name = row["first_name"]
|
|
9
|
+
email = row["email_address"]
|
|
10
|
+
phone = row.get("phone_number")
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Use:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from schemabind import bind
|
|
17
|
+
|
|
18
|
+
customer = bind(row)
|
|
19
|
+
|
|
20
|
+
customer.first_name
|
|
21
|
+
customer.email_address
|
|
22
|
+
customer.phone_number
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
SchemaBind is currently focused on dictionaries, including rows produced by `csv.DictReader`.
|
|
26
|
+
|
|
27
|
+
It does not create schemas, validate data, transform values, or infer new fields. It simply provides a cleaner way to access fields that already exist.
|
|
28
|
+
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import csv
|
|
33
|
+
from schemabind import bind
|
|
34
|
+
|
|
35
|
+
with open("samples/csv/customer.csv") as f:
|
|
36
|
+
rows = list(csv.DictReader(f))
|
|
37
|
+
|
|
38
|
+
customer = bind(rows[0])
|
|
39
|
+
|
|
40
|
+
print(customer.first_name)
|
|
41
|
+
print(customer.email_address)
|
|
42
|
+
print(customer.phone_number)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Missing attributes raise `AttributeError` instead of silently returning `None`, which helps catch typos in field names.
|
|
46
|
+
|
|
47
|
+
## Field Normalization
|
|
48
|
+
|
|
49
|
+
SchemaBind supports simple field-name normalization for attribute access.
|
|
50
|
+
|
|
51
|
+
For example:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
customer = bind({"First Name": "Kaladin"})
|
|
55
|
+
|
|
56
|
+
print(customer.first_name)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The original dictionary keys are preserved for dictionary-style helpers:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
customer.keys()
|
|
63
|
+
customer.get("First Name")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If multiple fields normalize to the same attribute name, SchemaBind raises `ValueError` instead of guessing which field to use.
|
|
67
|
+
|
|
68
|
+
For example:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
bind({
|
|
72
|
+
"First Name": "Dalinar",
|
|
73
|
+
"first_name": "Shallan",
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Both fields would normalize to `first_name`, so the binding is rejected as ambiguous.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "schemabind"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Expose existing dictionary fields as readable Python attributes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Adam" }
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
keywords = ["dictionary", "attributes", "csv", "schema", "developer-tools"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/adamhx2/SchemaBind"
|
|
27
|
+
Issues = "https://github.com/adamhx2/SchemaBind/issues"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["schemabind"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from schemabind.core import bind
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class BoundRow:
|
|
2
|
+
def __init__(self, data):
|
|
3
|
+
self._data = data
|
|
4
|
+
self._normalized_keys = {}
|
|
5
|
+
|
|
6
|
+
for key in data.keys():
|
|
7
|
+
normalized_key = key.lower().replace(" ", "_")
|
|
8
|
+
if normalized_key in self._normalized_keys:
|
|
9
|
+
raise ValueError(
|
|
10
|
+
f"Duplicate normalized key '{normalized_key}' for original keys "
|
|
11
|
+
f"'{self._normalized_keys[normalized_key]}' and '{key}'"
|
|
12
|
+
)
|
|
13
|
+
self._normalized_keys[normalized_key] = key
|
|
14
|
+
|
|
15
|
+
def __getattr__(self, name):
|
|
16
|
+
if name not in self._normalized_keys:
|
|
17
|
+
available_fields = ", ".join(self._data.keys())
|
|
18
|
+
raise AttributeError(
|
|
19
|
+
f"Field '{name}' not found. " f"Available fields: {available_fields}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
original_key = self._normalized_keys[name]
|
|
23
|
+
return self._data[original_key]
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"BoundRow(keys={list(self._data.keys())})"
|
|
27
|
+
|
|
28
|
+
def keys(self):
|
|
29
|
+
return self._data.keys()
|
|
30
|
+
|
|
31
|
+
def values(self):
|
|
32
|
+
return self._data.values()
|
|
33
|
+
|
|
34
|
+
def items(self):
|
|
35
|
+
return self._data.items()
|
|
36
|
+
|
|
37
|
+
def get(self, key, default=None):
|
|
38
|
+
return self._data.get(key, default)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bind(data):
|
|
42
|
+
if not isinstance(data, dict):
|
|
43
|
+
raise TypeError(f"Expected dict, got {type(data).__name__}")
|
|
44
|
+
|
|
45
|
+
return BoundRow(data)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schemabind
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Expose existing dictionary fields as readable Python attributes.
|
|
5
|
+
Author: Adam
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/adamhx2/SchemaBind
|
|
8
|
+
Project-URL: Issues, https://github.com/adamhx2/SchemaBind/issues
|
|
9
|
+
Keywords: dictionary,attributes,csv,schema,developer-tools
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# SchemaBind
|
|
20
|
+
|
|
21
|
+
SchemaBind reduces repetitive dictionary lookup boilerplate by exposing existing fields as readable Python attributes.
|
|
22
|
+
|
|
23
|
+
Instead of:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
first_name = row["first_name"]
|
|
27
|
+
email = row["email_address"]
|
|
28
|
+
phone = row.get("phone_number")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from schemabind import bind
|
|
35
|
+
|
|
36
|
+
customer = bind(row)
|
|
37
|
+
|
|
38
|
+
customer.first_name
|
|
39
|
+
customer.email_address
|
|
40
|
+
customer.phone_number
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
SchemaBind is currently focused on dictionaries, including rows produced by `csv.DictReader`.
|
|
44
|
+
|
|
45
|
+
It does not create schemas, validate data, transform values, or infer new fields. It simply provides a cleaner way to access fields that already exist.
|
|
46
|
+
|
|
47
|
+
## Example
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import csv
|
|
51
|
+
from schemabind import bind
|
|
52
|
+
|
|
53
|
+
with open("samples/csv/customer.csv") as f:
|
|
54
|
+
rows = list(csv.DictReader(f))
|
|
55
|
+
|
|
56
|
+
customer = bind(rows[0])
|
|
57
|
+
|
|
58
|
+
print(customer.first_name)
|
|
59
|
+
print(customer.email_address)
|
|
60
|
+
print(customer.phone_number)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Missing attributes raise `AttributeError` instead of silently returning `None`, which helps catch typos in field names.
|
|
64
|
+
|
|
65
|
+
## Field Normalization
|
|
66
|
+
|
|
67
|
+
SchemaBind supports simple field-name normalization for attribute access.
|
|
68
|
+
|
|
69
|
+
For example:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
customer = bind({"First Name": "Kaladin"})
|
|
73
|
+
|
|
74
|
+
print(customer.first_name)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The original dictionary keys are preserved for dictionary-style helpers:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
customer.keys()
|
|
81
|
+
customer.get("First Name")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If multiple fields normalize to the same attribute name, SchemaBind raises `ValueError` instead of guessing which field to use.
|
|
85
|
+
|
|
86
|
+
For example:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
bind({
|
|
90
|
+
"First Name": "Dalinar",
|
|
91
|
+
"first_name": "Shallan",
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Both fields would normalize to `first_name`, so the binding is rejected as ambiguous.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
schemabind
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from schemabind import bind
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_attribute_access_returns_value():
|
|
6
|
+
row = bind({"first_name": "Kaladin"})
|
|
7
|
+
|
|
8
|
+
assert row.first_name == "Kaladin"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_missing_attribute_raises_attribute_error():
|
|
12
|
+
row = bind({"first_name": "Kaladin"})
|
|
13
|
+
|
|
14
|
+
with pytest.raises(AttributeError):
|
|
15
|
+
row.frist_name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_duplicate_normalized_keys():
|
|
19
|
+
# Ambiguous normalized names are rejected instead of guessed.
|
|
20
|
+
with pytest.raises(ValueError):
|
|
21
|
+
bind({"first_name": "Dalinar", "First Name": "Adolin"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_keys_values_items():
|
|
25
|
+
data = {"first_name": "Dalinar", "last_name": "Kholin"}
|
|
26
|
+
row = bind(data)
|
|
27
|
+
|
|
28
|
+
assert set(row.keys()) == set(data.keys())
|
|
29
|
+
assert set(row.values()) == set(data.values())
|
|
30
|
+
assert set(row.items()) == set(data.items())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_get_method():
|
|
34
|
+
data = {"first_name": "Shallan", "last_name": "Davar"}
|
|
35
|
+
row = bind(data)
|
|
36
|
+
|
|
37
|
+
assert row.get("first_name") == "Shallan"
|
|
38
|
+
assert row.get("last_name") == "Davar"
|
|
39
|
+
assert row.get("non_existent", "bridge_four") == "bridge_four"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_non_dict_input():
|
|
43
|
+
with pytest.raises(TypeError):
|
|
44
|
+
bind(["not", "a", "dict"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_normalization():
|
|
48
|
+
# Spaces and case are normalized for attribute access.
|
|
49
|
+
row = bind({"First Name": "Adolin", "Last Name": "Kholin"})
|
|
50
|
+
|
|
51
|
+
assert row.first_name == "Adolin"
|
|
52
|
+
assert row.last_name == "Kholin"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_get_preserves_original_keys_after_normalization():
|
|
56
|
+
row = bind({"First Name": "Navani"})
|
|
57
|
+
|
|
58
|
+
assert row.first_name == "Navani"
|
|
59
|
+
assert row.get("First Name") == "Navani"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_keys_preserve_original_names_after_normalization():
|
|
63
|
+
row = bind({"First Name": "Jasnah"})
|
|
64
|
+
|
|
65
|
+
assert "First Name" in row.keys()
|
|
66
|
+
assert "first_name" not in row.keys()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_empty_string_values_are_valid():
|
|
70
|
+
# Empty strings are real values, not missing fields.
|
|
71
|
+
row = bind({"spren_name": ""})
|
|
72
|
+
|
|
73
|
+
assert row.spren_name == ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_duplicate_normalized_key_error_names_conflict():
|
|
77
|
+
# Collision errors name the normalized key and conflicting fields.
|
|
78
|
+
with pytest.raises(ValueError) as error:
|
|
79
|
+
bind({"First Name": "Dalinar", "first_name": "Adolin"})
|
|
80
|
+
|
|
81
|
+
message = str(error.value)
|
|
82
|
+
assert "first_name" in message
|
|
83
|
+
assert "First Name" in message
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_missing_attribute_error_names_available_fields():
|
|
87
|
+
# Missing-attribute errors name the request and available fields.
|
|
88
|
+
row = bind({"ideal": "Life before death"})
|
|
89
|
+
|
|
90
|
+
with pytest.raises(AttributeError) as error:
|
|
91
|
+
row.oath
|
|
92
|
+
|
|
93
|
+
message = str(error.value)
|
|
94
|
+
assert "oath" in message
|
|
95
|
+
assert "ideal" in message
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_repr_includes_keys():
|
|
99
|
+
row = bind({"radiant_order": "Windrunner"})
|
|
100
|
+
|
|
101
|
+
assert "radiant_order" in repr(row)
|