datavane 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.
- datavane-0.1.0/LICENSE +21 -0
- datavane-0.1.0/PKG-INFO +166 -0
- datavane-0.1.0/README.md +144 -0
- datavane-0.1.0/pyproject.toml +49 -0
- datavane-0.1.0/src/datavane/__init__.py +5 -0
- datavane-0.1.0/src/datavane/formatting.py +155 -0
- datavane-0.1.0/src/datavane/generator.py +27 -0
- datavane-0.1.0/src/datavane/inference.py +97 -0
- datavane-0.1.0/src/datavane/io.py +79 -0
- datavane-0.1.0/src/datavane/models.py +34 -0
datavane-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antonio Jesús Salido Ranea
|
|
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.
|
datavane-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datavane
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for automatic dataset documentation
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Antonio Jesús Salido Ranea
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest ; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff ; extra == "dev"
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# datavane
|
|
23
|
+
|
|
24
|
+
A Python library for automatically generating documentation from datasets.
|
|
25
|
+
|
|
26
|
+
`datavane` inspects JSON and CSV datasets, infers their structure and generates a Markdown data dictionary containing information about fields, types, examples, presence and null values.
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* Support for JSON and CSV files.
|
|
31
|
+
* Recursive processing of nested dictionaries and lists.
|
|
32
|
+
* Automatic field type inference.
|
|
33
|
+
* Field path notation for nested structures.
|
|
34
|
+
* Example values for detected fields.
|
|
35
|
+
* Field presence and null-value statistics.
|
|
36
|
+
* Recursive discovery of JSON and CSV files inside directories.
|
|
37
|
+
* Markdown documentation generation.
|
|
38
|
+
* No external runtime dependencies.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Install Datavane from PyPI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install datavane
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Once installed, you can import it directly in your Python project:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from datavane import doc_table
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
The main public API is `doc_table()`:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
from data_dictionary import doc_table
|
|
63
|
+
|
|
64
|
+
doc_table(
|
|
65
|
+
Path("data/matches.json"),
|
|
66
|
+
"Matches",
|
|
67
|
+
Path("docs/matches.md"),
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The function accepts either a JSON/CSV file or a directory containing supported datasets.
|
|
72
|
+
|
|
73
|
+
For example, given a dataset containing:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"id": 1,
|
|
78
|
+
"name": "Juan",
|
|
79
|
+
"address": {
|
|
80
|
+
"city": "Málaga"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
the generated documentation can identify fields such as:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
id
|
|
89
|
+
name
|
|
90
|
+
address
|
|
91
|
+
address.city
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
along with their inferred types, examples and statistics.
|
|
95
|
+
|
|
96
|
+
## Generated documentation
|
|
97
|
+
|
|
98
|
+
The generated Markdown document contains a summary followed by the inferred schema:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
# Matches
|
|
102
|
+
|
|
103
|
+
> Data dictionary generated automatically by `datavane`.
|
|
104
|
+
|
|
105
|
+
## Dataset summary
|
|
106
|
+
|
|
107
|
+
| Property | Value |
|
|
108
|
+
|----------|-------|
|
|
109
|
+
| Records | 100 |
|
|
110
|
+
| Fields | 8 |
|
|
111
|
+
|
|
112
|
+
## Schema
|
|
113
|
+
|
|
114
|
+
| Field | Type | Example | Presence | Nulls |
|
|
115
|
+
|---|---|---|---|---|
|
|
116
|
+
| id | int | 1 | 100.0% | 0.0% |
|
|
117
|
+
| name | str | John | 100.0% | 0.0% |
|
|
118
|
+
| address.city | str | Madrid | 98.0% | 0.0% |
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Project structure
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
data-dictionary/
|
|
125
|
+
├── src/
|
|
126
|
+
│ └── data_dictionary/
|
|
127
|
+
│ ├── __init__.py
|
|
128
|
+
│ ├── models.py
|
|
129
|
+
│ ├── inference.py
|
|
130
|
+
│ ├── formatting.py
|
|
131
|
+
│ ├── io.py
|
|
132
|
+
│ └── generator.py
|
|
133
|
+
├── tests/
|
|
134
|
+
│ ├── fixtures/
|
|
135
|
+
│ ├── unit/
|
|
136
|
+
│ └── integration/
|
|
137
|
+
├── pyproject.toml
|
|
138
|
+
├── README.md
|
|
139
|
+
└── LICENSE
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
Create a virtual environment and install the project in editable mode:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python -m venv .venv
|
|
148
|
+
python -m pip install -e .
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Run the test suite with:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The project contains both unit and integration tests.
|
|
158
|
+
|
|
159
|
+
## Status
|
|
160
|
+
|
|
161
|
+
This project is currently in early development. The public API and internal implementation may change as the library evolves.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This project is licensed under the MIT License.
|
|
166
|
+
|
datavane-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# datavane
|
|
2
|
+
|
|
3
|
+
A Python library for automatically generating documentation from datasets.
|
|
4
|
+
|
|
5
|
+
`datavane` inspects JSON and CSV datasets, infers their structure and generates a Markdown data dictionary containing information about fields, types, examples, presence and null values.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Support for JSON and CSV files.
|
|
10
|
+
* Recursive processing of nested dictionaries and lists.
|
|
11
|
+
* Automatic field type inference.
|
|
12
|
+
* Field path notation for nested structures.
|
|
13
|
+
* Example values for detected fields.
|
|
14
|
+
* Field presence and null-value statistics.
|
|
15
|
+
* Recursive discovery of JSON and CSV files inside directories.
|
|
16
|
+
* Markdown documentation generation.
|
|
17
|
+
* No external runtime dependencies.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Install Datavane from PyPI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install datavane
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Once installed, you can import it directly in your Python project:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from datavane import doc_table
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
The main public API is `doc_table()`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from data_dictionary import doc_table
|
|
42
|
+
|
|
43
|
+
doc_table(
|
|
44
|
+
Path("data/matches.json"),
|
|
45
|
+
"Matches",
|
|
46
|
+
Path("docs/matches.md"),
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The function accepts either a JSON/CSV file or a directory containing supported datasets.
|
|
51
|
+
|
|
52
|
+
For example, given a dataset containing:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"id": 1,
|
|
57
|
+
"name": "Juan",
|
|
58
|
+
"address": {
|
|
59
|
+
"city": "Málaga"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
the generated documentation can identify fields such as:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
id
|
|
68
|
+
name
|
|
69
|
+
address
|
|
70
|
+
address.city
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
along with their inferred types, examples and statistics.
|
|
74
|
+
|
|
75
|
+
## Generated documentation
|
|
76
|
+
|
|
77
|
+
The generated Markdown document contains a summary followed by the inferred schema:
|
|
78
|
+
|
|
79
|
+
```markdown
|
|
80
|
+
# Matches
|
|
81
|
+
|
|
82
|
+
> Data dictionary generated automatically by `datavane`.
|
|
83
|
+
|
|
84
|
+
## Dataset summary
|
|
85
|
+
|
|
86
|
+
| Property | Value |
|
|
87
|
+
|----------|-------|
|
|
88
|
+
| Records | 100 |
|
|
89
|
+
| Fields | 8 |
|
|
90
|
+
|
|
91
|
+
## Schema
|
|
92
|
+
|
|
93
|
+
| Field | Type | Example | Presence | Nulls |
|
|
94
|
+
|---|---|---|---|---|
|
|
95
|
+
| id | int | 1 | 100.0% | 0.0% |
|
|
96
|
+
| name | str | John | 100.0% | 0.0% |
|
|
97
|
+
| address.city | str | Madrid | 98.0% | 0.0% |
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Project structure
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
data-dictionary/
|
|
104
|
+
├── src/
|
|
105
|
+
│ └── data_dictionary/
|
|
106
|
+
│ ├── __init__.py
|
|
107
|
+
│ ├── models.py
|
|
108
|
+
│ ├── inference.py
|
|
109
|
+
│ ├── formatting.py
|
|
110
|
+
│ ├── io.py
|
|
111
|
+
│ └── generator.py
|
|
112
|
+
├── tests/
|
|
113
|
+
│ ├── fixtures/
|
|
114
|
+
│ ├── unit/
|
|
115
|
+
│ └── integration/
|
|
116
|
+
├── pyproject.toml
|
|
117
|
+
├── README.md
|
|
118
|
+
└── LICENSE
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
Create a virtual environment and install the project in editable mode:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
python -m venv .venv
|
|
127
|
+
python -m pip install -e .
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Run the test suite with:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pytest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The project contains both unit and integration tests.
|
|
137
|
+
|
|
138
|
+
## Status
|
|
139
|
+
|
|
140
|
+
This project is currently in early development. The public API and internal implementation may change as the library evolves.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=2.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "datavane"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Python library for automatic dataset documentation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Antonio Jesús Salido Ranea"}
|
|
14
|
+
]
|
|
15
|
+
dependencies = []
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest",
|
|
31
|
+
"ruff",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
line-length = 88
|
|
36
|
+
target-version = "py311"
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = [
|
|
40
|
+
"E",
|
|
41
|
+
"F",
|
|
42
|
+
"I",
|
|
43
|
+
"UP",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.poetry]
|
|
47
|
+
packages = [
|
|
48
|
+
{ include = "datavane", from = "src" }
|
|
49
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from .models import FieldInfo
|
|
2
|
+
|
|
3
|
+
MAX_DICT_FIELDS = 5
|
|
4
|
+
MAX_LIST_FIELDS = 3
|
|
5
|
+
MAX_STRING_LENGTH = 80
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_types(types: set[str]) -> str:
|
|
9
|
+
"""Format a set of types as a sorted, comma-separated string.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
types (set[str]): Set containing the inferred types of a field.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: A comma-separated string containing the types in sorted order.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
return ", ".join(sorted(types))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_presence(field: FieldInfo, total_records: int) -> str:
|
|
22
|
+
"""Calculate and format the percentage of records containing a field.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
field (FieldInfo): Field information containing the number of appearances.
|
|
26
|
+
total_records (int): Total number of records in the dataset.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: The field presence as a percentage string.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
return str(round((field.appearances / total_records) * 100, 1)) + "%"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def format_nulls(field: FieldInfo, total_records: int) -> str:
|
|
36
|
+
"""Calculate and format the percentage of null values for a field.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
field (FieldInfo): Field information containing the number of null values.
|
|
40
|
+
total_records (int): Total number of records in the dataset.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: The null value percentage as a string.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
return str(round((field.null_count / total_records) * 100, 1)) + "%"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_example(example) -> str:
|
|
50
|
+
"""Format an example value for inclusion in the Markdown output.
|
|
51
|
+
|
|
52
|
+
Strings longer than ``MAX_STRING_LENGTH`` are truncated. Dictionaries
|
|
53
|
+
and lists are recursively formatted and limited to the configured
|
|
54
|
+
number of elements.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
example: Example value to format.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
str: A string representation suitable for the generated documentation.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if example is None:
|
|
64
|
+
return "null"
|
|
65
|
+
|
|
66
|
+
if isinstance(example, str):
|
|
67
|
+
if len(example) > MAX_STRING_LENGTH:
|
|
68
|
+
return example[:MAX_STRING_LENGTH] + "..."
|
|
69
|
+
return example
|
|
70
|
+
|
|
71
|
+
if isinstance(example, dict):
|
|
72
|
+
examples_list = []
|
|
73
|
+
|
|
74
|
+
for i, (key, value) in enumerate(example.items()):
|
|
75
|
+
if i >= MAX_DICT_FIELDS:
|
|
76
|
+
examples_list.append("...")
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
examples_list.append(f"{key}: {format_example(value)}")
|
|
80
|
+
|
|
81
|
+
return "{ " + ", ".join(examples_list) + " }"
|
|
82
|
+
|
|
83
|
+
if isinstance(example, list):
|
|
84
|
+
examples_list = []
|
|
85
|
+
|
|
86
|
+
for i, entry in enumerate(example):
|
|
87
|
+
if i >= MAX_LIST_FIELDS:
|
|
88
|
+
examples_list.append("...")
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
examples_list.append(format_example(entry))
|
|
92
|
+
|
|
93
|
+
return "[ " + ", ".join(examples_list) + " ]"
|
|
94
|
+
|
|
95
|
+
return str(example)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def format_summary(
|
|
99
|
+
table_name: str, total_fields: int, total_records: int, file
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Write the dataset summary section to a file.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
table_name (str): Name of the dataset or table being documented.
|
|
105
|
+
total_fields (int): Number of fields detected in the dataset.
|
|
106
|
+
total_records (int): Number of records in the dataset.
|
|
107
|
+
file: File-like object where the Markdown output is written.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
file.write(f"# {table_name} \n")
|
|
111
|
+
file.write("> Data dictionary generated automatically by `datavane` \n")
|
|
112
|
+
file.write("## Dataset summary \n")
|
|
113
|
+
file.write("| Property | Value | \n")
|
|
114
|
+
file.write("|----------|-------| \n")
|
|
115
|
+
file.write(f"| Records | {total_records}| \n")
|
|
116
|
+
file.write(f"| Fields | {total_fields} | \n")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def markdown_row(field: FieldInfo, total_records: int) -> str:
|
|
120
|
+
"""Format a field as a Markdown table row.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
field (FieldInfo): Field information to format.
|
|
124
|
+
total_records (int): Total number of records in the dataset.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
str: A Markdown table row containing the field's type, example, presence,
|
|
128
|
+
and null percentage.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
f"| {field.name} | "
|
|
133
|
+
f"{format_types(field.types)} | "
|
|
134
|
+
f"{format_example(field.example)} | "
|
|
135
|
+
f"{format_presence(field, total_records)} | "
|
|
136
|
+
f"{format_nulls(field, total_records)} |\n"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def markdown_table(fields_dict: dict[str, FieldInfo], total_records: int, file) -> None:
|
|
141
|
+
"""Write the dataset schema as a Markdown table.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
fields_dict (dict[str, FieldInfo]): Mapping of field names
|
|
145
|
+
to their inferred information.
|
|
146
|
+
total_records (int): Total number of records in the dataset.
|
|
147
|
+
file: File-like object where the Markdown output is written.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
file.write("## Schema \n")
|
|
151
|
+
file.write("| Field | Type | Example | Presence | Nulls | \n")
|
|
152
|
+
file.write("|-------|------|---------| -------- | ----- |\n")
|
|
153
|
+
file.writelines(
|
|
154
|
+
markdown_row(field, total_records) for field in fields_dict.values()
|
|
155
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .formatting import format_summary, markdown_table
|
|
4
|
+
from .inference import infer_schema
|
|
5
|
+
from .io import load_dataset
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def doc_table(path: Path, table_name: str, output_path: Path) -> None:
|
|
9
|
+
"""Generate a Markdown data dictionary for a dataset.
|
|
10
|
+
|
|
11
|
+
The dataset is loaded from the given path, its schema is inferred,
|
|
12
|
+
and the resulting documentation is written to the specified output
|
|
13
|
+
file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
path (Path): Path to the input dataset or directory containing datasets.
|
|
17
|
+
table_name (str): Name used as the title of the generated documentation.
|
|
18
|
+
output_path (Path): Path where the Markdown documentation will be written.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
data = load_dataset(path)
|
|
22
|
+
total_records = len(data)
|
|
23
|
+
schema = infer_schema(data)
|
|
24
|
+
total_fields = len(schema)
|
|
25
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
26
|
+
format_summary(table_name, total_fields, total_records, f)
|
|
27
|
+
markdown_table(schema, total_records, f)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from .models import FieldInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def infer_type(value) -> str:
|
|
5
|
+
"""Infer the type of a value, including nested lists.
|
|
6
|
+
|
|
7
|
+
Lists are represented using the inferred types of their elements.
|
|
8
|
+
Empty lists are represented simply as ``list``.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
value: Value whose type should be inferred.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: A string representing the inferred type.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
if value is None:
|
|
18
|
+
return "null"
|
|
19
|
+
|
|
20
|
+
if isinstance(value, list):
|
|
21
|
+
if not value:
|
|
22
|
+
return "list"
|
|
23
|
+
|
|
24
|
+
element_types = set()
|
|
25
|
+
|
|
26
|
+
for element in value:
|
|
27
|
+
element_types.add(infer_type(element))
|
|
28
|
+
|
|
29
|
+
return f"list[{', '.join(sorted(element_types))}]"
|
|
30
|
+
|
|
31
|
+
return type(value).__name__
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def process_object(
|
|
35
|
+
obj: dict | list,
|
|
36
|
+
fields: dict[str, FieldInfo],
|
|
37
|
+
prefix: str = "",
|
|
38
|
+
context_count: int = 1,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Recursively process a dictionary or list to infer field information.
|
|
41
|
+
|
|
42
|
+
Nested fields are represented using dot notation, while fields inside
|
|
43
|
+
lists are marked with ``[]``. Field statistics are updated as values
|
|
44
|
+
are encountered.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
obj (dict | list): Dictionary or list to process.
|
|
48
|
+
fields (dict[str, FieldInfo]): Mapping of field paths to their corresponding
|
|
49
|
+
``FieldInfo``.
|
|
50
|
+
prefix (str, optional): Path prefix used for nested fields. Defaults to "".
|
|
51
|
+
context_count (int, optional): Number of records represented by the current
|
|
52
|
+
context. Defaults to 1.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if isinstance(obj, dict):
|
|
56
|
+
for field, value in obj.items():
|
|
57
|
+
if prefix:
|
|
58
|
+
field_name = f"{prefix}.{field}"
|
|
59
|
+
else:
|
|
60
|
+
field_name = field
|
|
61
|
+
|
|
62
|
+
if field_name not in fields:
|
|
63
|
+
fields[field_name] = FieldInfo(
|
|
64
|
+
name=field_name, context_count=context_count
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
fields[field_name].update(value, infer_type(value))
|
|
68
|
+
|
|
69
|
+
if isinstance(value, (dict, list)):
|
|
70
|
+
process_object(value, fields, field_name, context_count=1)
|
|
71
|
+
|
|
72
|
+
elif isinstance(obj, list):
|
|
73
|
+
for entry in obj:
|
|
74
|
+
if isinstance(entry, dict):
|
|
75
|
+
process_object(entry, fields, prefix + "[]", context_count=len(obj))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def infer_schema(data: list[dict]) -> dict[str, FieldInfo]:
|
|
79
|
+
"""Infer the schema of a dataset.
|
|
80
|
+
|
|
81
|
+
Each record is recursively processed to collect information about
|
|
82
|
+
its fields, including their types, examples, appearances, nulls,
|
|
83
|
+
and contextual occurrence counts.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
data (list[dict]): Dataset represented as a list of dictionaries.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
dict[str, FieldInfo]: A dictionary mapping field paths to their inferred
|
|
90
|
+
``FieldInfo``.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
fields: dict[str, FieldInfo] = {}
|
|
94
|
+
for entry in data:
|
|
95
|
+
process_object(entry, fields, context_count=len(data))
|
|
96
|
+
|
|
97
|
+
return dict(sorted(fields.items()))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from csv import DictReader
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
SUPPORTED_EXTENSIONS = {".json", ".csv"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_csv(file_path: Path) -> list[dict]:
|
|
9
|
+
"""Load records from a CSV file.
|
|
10
|
+
|
|
11
|
+
The first row of the CSV file is used as the field names.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
file_path (Path): Path to the CSV file.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
list[dict]: A list of dictionaries containing the CSV records.
|
|
18
|
+
"""
|
|
19
|
+
with open(file_path, newline="", encoding="utf-8") as csvfile:
|
|
20
|
+
reader = DictReader(csvfile)
|
|
21
|
+
return list(reader)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_json(file_path: Path) -> list[dict]:
|
|
25
|
+
"""Load records from a JSON file.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
file_path (Path): Path to the JSON file.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
list[dict]: The records contained in the JSON file.
|
|
32
|
+
"""
|
|
33
|
+
with open(file_path, encoding="utf-8") as jsonfile:
|
|
34
|
+
data = json.load(jsonfile)
|
|
35
|
+
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_dataset(path: Path) -> list[dict]:
|
|
40
|
+
"""Load a dataset from a file or directory.
|
|
41
|
+
|
|
42
|
+
JSON and CSV files are supported. When a directory is provided,
|
|
43
|
+
supported files are searched recursively and their records are
|
|
44
|
+
combined into a single dataset.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path (Path): Path to a JSON/CSV file or a directory containing them.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If a file has an unsupported extension.
|
|
51
|
+
FileNotFoundError: If the provided path does not exist or is neither a file
|
|
52
|
+
nor a directory.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
list[dict]: A list of dictionaries containing the dataset records.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if path.is_file():
|
|
59
|
+
if path.suffix.lower() == ".json":
|
|
60
|
+
return load_json(path)
|
|
61
|
+
elif path.suffix.lower() == ".csv":
|
|
62
|
+
return load_csv(path)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"File path {path} not supported. File extension must be .csv or .json"
|
|
66
|
+
)
|
|
67
|
+
elif path.is_dir():
|
|
68
|
+
data = []
|
|
69
|
+
for file in path.rglob("*"):
|
|
70
|
+
if file.suffix.lower() not in SUPPORTED_EXTENSIONS:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if file.suffix.lower() == ".json":
|
|
74
|
+
data.extend(load_json(file))
|
|
75
|
+
elif file.suffix.lower() == ".csv":
|
|
76
|
+
data.extend(load_csv(file))
|
|
77
|
+
return data
|
|
78
|
+
else:
|
|
79
|
+
raise FileNotFoundError(f"The path {path} doesn't exist")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class FieldInfo:
|
|
6
|
+
"""Store inferred information and statistics for a dataset field."""
|
|
7
|
+
|
|
8
|
+
name: str
|
|
9
|
+
types: set[str] = field(default_factory=set)
|
|
10
|
+
example: object | None = None
|
|
11
|
+
appearances: int = 0
|
|
12
|
+
null_count: int = 0
|
|
13
|
+
context_count: int = 0
|
|
14
|
+
|
|
15
|
+
def update(self, new_value, value_type) -> None:
|
|
16
|
+
"""Update the field information with a new observed value.
|
|
17
|
+
|
|
18
|
+
The value type and appearance count are always updated. Null values
|
|
19
|
+
increment the null counter, while the first non-null value is stored
|
|
20
|
+
as the field example.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
new_value: Value observed for the field.
|
|
24
|
+
value_type: Inferred type of the observed value.
|
|
25
|
+
"""
|
|
26
|
+
self.types.add(value_type)
|
|
27
|
+
self.appearances += 1
|
|
28
|
+
|
|
29
|
+
if new_value is None:
|
|
30
|
+
self.null_count += 1
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if self.example is None:
|
|
34
|
+
self.example = new_value
|