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 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.
@@ -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
+
@@ -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,5 @@
1
+ from .generator import doc_table
2
+
3
+ __all__ = [
4
+ "doc_table",
5
+ ]
@@ -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