datafun-streaming 0.1.0__py3-none-any.whl
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.
- datafun_streaming/__init__.py +1 -0
- datafun_streaming/_version.py +24 -0
- datafun_streaming/core/__init__.py +1 -0
- datafun_streaming/core/types.py +16 -0
- datafun_streaming/data_validation/__init__.py +1 -0
- datafun_streaming/data_validation/errors.py +24 -0
- datafun_streaming/data_validation/reference.py +63 -0
- datafun_streaming/data_validation/types.py +42 -0
- datafun_streaming/data_validation/validation_utils.py +143 -0
- datafun_streaming/io/__init__.py +1 -0
- datafun_streaming/io/errors.py +50 -0
- datafun_streaming/io/io_utils.py +109 -0
- datafun_streaming/kafka/__init__.py +1 -0
- datafun_streaming/kafka/errors.py +150 -0
- datafun_streaming/kafka/kafka_admin_utils.py +211 -0
- datafun_streaming/kafka/kafka_connection_utils.py +46 -0
- datafun_streaming/kafka/kafka_consumer_utils.py +62 -0
- datafun_streaming/kafka/kafka_producer_utils.py +96 -0
- datafun_streaming/kafka/kafka_settings.py +79 -0
- datafun_streaming/py.typed +0 -0
- datafun_streaming/stats/__init__.py +1 -0
- datafun_streaming/stats/stats_utils.py +110 -0
- datafun_streaming/storage/__init__.py +1 -0
- datafun_streaming/storage/duckdb_utils.py +244 -0
- datafun_streaming/visualization/__init__.py +1 -0
- datafun_streaming/visualization/chart_utils.py +150 -0
- datafun_streaming-0.1.0.dist-info/METADATA +168 -0
- datafun_streaming-0.1.0.dist-info/RECORD +30 -0
- datafun_streaming-0.1.0.dist-info/WHEEL +4 -0
- datafun_streaming-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared streaming utilities for Kafka, DuckDB, validation, and visualization."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared primitives."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""src/datafun_streaming/core/types.py.
|
|
2
|
+
|
|
3
|
+
Shared type aliases used across all datafun_streaming subpackages.
|
|
4
|
+
Import from here when type-hinting streaming records in any module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DataRecordDict",
|
|
9
|
+
"DataRecordDictList",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# One message / row / record as a dictionary of text values.
|
|
13
|
+
DataRecordDict = dict[str, str]
|
|
14
|
+
|
|
15
|
+
# A list of messages / rows / records.
|
|
16
|
+
DataRecordDictList = list[DataRecordDict]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data validation utilities, types, and error messages for streaming pipelines."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""data_validation/errors.py.
|
|
2
|
+
|
|
3
|
+
Error messages for validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def reference_validation_failed_message(*, label: str, error_count: int) -> str:
|
|
8
|
+
"""Return help text when a reference data file fails validation."""
|
|
9
|
+
return f"""
|
|
10
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
11
|
+
A reference data file failed validation.
|
|
12
|
+
File: {label}
|
|
13
|
+
Errors: {error_count} problem(s) found.
|
|
14
|
+
|
|
15
|
+
The producer cannot run until all reference files are valid.
|
|
16
|
+
Fix the reference file before retrying.
|
|
17
|
+
|
|
18
|
+
CHECK:
|
|
19
|
+
1. Open data/{label} and inspect the header row.
|
|
20
|
+
2. Confirm all required fields are present and spelled correctly.
|
|
21
|
+
3. Confirm no rows have blank values in required fields.
|
|
22
|
+
4. See data_contract_case.py for the list of required fields.
|
|
23
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
24
|
+
""".strip()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""src/datafun_streaming/data_validation/reference.py.
|
|
2
|
+
|
|
3
|
+
Reference data validation helpers.
|
|
4
|
+
|
|
5
|
+
Provides functions for working with lookup tables:
|
|
6
|
+
building lookup sets from CSV rows and validating reference records.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# === IMPORTS ===
|
|
10
|
+
|
|
11
|
+
from datafun_streaming.core.types import DataRecordDictList
|
|
12
|
+
from datafun_streaming.data_validation.types import AllowedValuesSet
|
|
13
|
+
from datafun_streaming.data_validation.validation_utils import validate_required_fields
|
|
14
|
+
|
|
15
|
+
# === EXPORTS ===
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"make_lookup_set",
|
|
19
|
+
"validate_reference_records",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_lookup_set(records: DataRecordDictList, key_field: str) -> AllowedValuesSet:
|
|
24
|
+
"""Create a set of allowed values for a field in a reference table.
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
records: A list of row dictionaries from a reference CSV file.
|
|
28
|
+
key_field: The field to use as the key for allowed values.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A set of allowed values for the specified key field.
|
|
32
|
+
"""
|
|
33
|
+
values: AllowedValuesSet = set()
|
|
34
|
+
for record in records:
|
|
35
|
+
value: str = record.get(key_field, "").strip()
|
|
36
|
+
if value:
|
|
37
|
+
values.add(value)
|
|
38
|
+
return values
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_reference_records(
|
|
42
|
+
*,
|
|
43
|
+
records: DataRecordDictList,
|
|
44
|
+
required_fields: list[str],
|
|
45
|
+
label: str,
|
|
46
|
+
) -> list[str]:
|
|
47
|
+
"""Validate reference records and return file-level errors.
|
|
48
|
+
|
|
49
|
+
Arguments:
|
|
50
|
+
records: Reference data records to validate.
|
|
51
|
+
required_fields: Field names required in each record.
|
|
52
|
+
label: Label for this reference file, used in error messages.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A list of errors, or an empty list if all records are valid.
|
|
56
|
+
"""
|
|
57
|
+
errors: list[str] = []
|
|
58
|
+
for record_number, record in enumerate(records, start=1):
|
|
59
|
+
for error in validate_required_fields(
|
|
60
|
+
record=record, required_fields=required_fields
|
|
61
|
+
):
|
|
62
|
+
errors.append(f"{label} record {record_number}: {error}")
|
|
63
|
+
return errors
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""src/datafun_streaming/data_validation/types.py.
|
|
2
|
+
|
|
3
|
+
Type aliases and dataclasses for validation results.
|
|
4
|
+
|
|
5
|
+
Import from here whenever you need to type-hint a record or validation result.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# === IMPORTS ===
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from datafun_streaming.core.types import DataRecordDict, DataRecordDictList
|
|
13
|
+
|
|
14
|
+
# === EXPORTS ===
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DataRecordDict",
|
|
18
|
+
"DataRecordDictList",
|
|
19
|
+
"ErrorMessage",
|
|
20
|
+
"ErrorMessages",
|
|
21
|
+
"AllowedValuesSet",
|
|
22
|
+
"ValidationResult",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# === TYPE ALIASES ===
|
|
26
|
+
|
|
27
|
+
ErrorMessage = str
|
|
28
|
+
ErrorMessages = list[ErrorMessage]
|
|
29
|
+
AllowedValuesSet = set[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ValidationResult:
|
|
34
|
+
"""Result from checking one record against the data contract.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
is_valid: True if the record passed all validation checks.
|
|
38
|
+
errors: List of error messages; empty when is_valid is True.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
is_valid: bool
|
|
42
|
+
errors: ErrorMessages
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""src/datafun_streaming/data_validation/validation_utils.py.
|
|
2
|
+
|
|
3
|
+
Generic field-level validation functions.
|
|
4
|
+
|
|
5
|
+
Each function checks one thing about one value and returns a list of
|
|
6
|
+
error strings, empty if valid, one or more messages if not.
|
|
7
|
+
These functions know nothing about domains, reference data, or business rules.
|
|
8
|
+
They only check types, formats, and value ranges.
|
|
9
|
+
|
|
10
|
+
OBS:
|
|
11
|
+
Add functions to this file as validation requirements evolve.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# == IMPORTS ==
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
from datafun_streaming.core.types import DataRecordDict
|
|
19
|
+
from datafun_streaming.data_validation.types import ErrorMessages
|
|
20
|
+
|
|
21
|
+
# == EXPORTS ==
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"add_validation_errors",
|
|
25
|
+
"validate_boolean_text",
|
|
26
|
+
"validate_datetime",
|
|
27
|
+
"validate_positive_integer",
|
|
28
|
+
"validate_required_fields",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_validation_errors(
|
|
33
|
+
*,
|
|
34
|
+
record: DataRecordDict,
|
|
35
|
+
errors: ErrorMessages,
|
|
36
|
+
) -> DataRecordDict:
|
|
37
|
+
"""Return a copy of a record with validation errors attached.
|
|
38
|
+
|
|
39
|
+
Arguments:
|
|
40
|
+
record: A dictionary representing one data record.
|
|
41
|
+
errors: A list of validation error messages.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A copy of the record with a validation_errors field appended.
|
|
45
|
+
"""
|
|
46
|
+
output = dict(record)
|
|
47
|
+
output["validation_errors"] = " | ".join(errors)
|
|
48
|
+
return output
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_boolean_text(value: str, *, field_name: str) -> list[str]:
|
|
52
|
+
"""Return errors for an invalid boolean text value.
|
|
53
|
+
|
|
54
|
+
All boolean values must be represented as
|
|
55
|
+
"true" or "false" (case-insensitive).
|
|
56
|
+
|
|
57
|
+
Arguments:
|
|
58
|
+
value: The text value to validate.
|
|
59
|
+
*: All arguments after the asterisk must be passed as keyword arguments.
|
|
60
|
+
field_name: The name of the field being validated, for error messages.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A list of errors, or an empty list if the value is valid.
|
|
64
|
+
"""
|
|
65
|
+
allowed_values = {"true", "false"}
|
|
66
|
+
|
|
67
|
+
if value.lower() not in allowed_values:
|
|
68
|
+
return [f"{field_name} must be true or false: {value}"]
|
|
69
|
+
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def validate_datetime(value: str) -> list[str]:
|
|
74
|
+
"""Return errors for an invalid datetime value.
|
|
75
|
+
|
|
76
|
+
All datetime values must be in ISO 8601 format.
|
|
77
|
+
|
|
78
|
+
Arguments:
|
|
79
|
+
value: The text value to validate.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A list of errors, or an empty list if the value is valid.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
86
|
+
except ValueError:
|
|
87
|
+
return [f"Invalid datetime: {value}"]
|
|
88
|
+
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_positive_integer(value: str) -> list[str]:
|
|
93
|
+
"""Return errors for an invalid positive integer value.
|
|
94
|
+
|
|
95
|
+
All positive integer values must be integers greater than or equal to 1.
|
|
96
|
+
|
|
97
|
+
Arguments:
|
|
98
|
+
value: The text value to validate.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A list of errors, or an empty list if the value is valid.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
number = int(value)
|
|
105
|
+
except ValueError:
|
|
106
|
+
return [f"Value must be an integer: {value}"]
|
|
107
|
+
|
|
108
|
+
if number < 1:
|
|
109
|
+
return [f"Value must be at least 1: {value}"]
|
|
110
|
+
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# === DEFINE FIELD VALIDATION HELPERS ===
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_required_fields(
|
|
118
|
+
*,
|
|
119
|
+
record: DataRecordDict,
|
|
120
|
+
required_fields: list[str],
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""Return errors for missing or blank required fields.
|
|
123
|
+
|
|
124
|
+
All required fields must be present and not blank.
|
|
125
|
+
|
|
126
|
+
Arguments:
|
|
127
|
+
record: A dictionary representing one data record / row.
|
|
128
|
+
*: All arguments after the asterisk must be passed as keyword arguments.
|
|
129
|
+
required_fields: A list of field names that are required.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A list of errors, or
|
|
133
|
+
an empty list if all required fields are present.
|
|
134
|
+
"""
|
|
135
|
+
errors: list[str] = []
|
|
136
|
+
|
|
137
|
+
for field_name in required_fields:
|
|
138
|
+
if field_name not in record:
|
|
139
|
+
errors.append(f"Missing required field: {field_name}")
|
|
140
|
+
elif not record[field_name].strip():
|
|
141
|
+
errors.append(f"Required field is blank: {field_name}")
|
|
142
|
+
|
|
143
|
+
return errors
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""File I/O utilities for reading and writing data formats."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""io/errors.py."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# === EXPORTS ===
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"missing_csv_file_message",
|
|
8
|
+
"missing_csv_field_message",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
# === DEFINE HELPER FUNCTIONS ===
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def missing_csv_file_message(*, path: str) -> str:
|
|
15
|
+
"""Return help text for a missing CSV file."""
|
|
16
|
+
return f"""
|
|
17
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
18
|
+
This project needs a CSV file to generate messages.
|
|
19
|
+
Required CSV file not found:
|
|
20
|
+
{path}
|
|
21
|
+
|
|
22
|
+
CHECK:
|
|
23
|
+
1. Confirm you are running the command from the project root folder.
|
|
24
|
+
2. Confirm the data folder exists.
|
|
25
|
+
3. Confirm data/sales.csv exists.
|
|
26
|
+
4. If the file was deleted, restore it from the repository.
|
|
27
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
28
|
+
""".strip()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def missing_csv_field_message(*, field: str, available_fields: list[str]) -> str:
|
|
32
|
+
"""Return help text for a missing CSV field."""
|
|
33
|
+
fields = ", ".join(available_fields)
|
|
34
|
+
|
|
35
|
+
return f"""
|
|
36
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
37
|
+
The project read the CSV file,
|
|
38
|
+
but an expected column was not present.
|
|
39
|
+
Required CSV field missing:
|
|
40
|
+
{field}
|
|
41
|
+
|
|
42
|
+
Available fields were:
|
|
43
|
+
{fields}
|
|
44
|
+
|
|
45
|
+
CHECK:
|
|
46
|
+
1. Open data/sales.csv.
|
|
47
|
+
2. Confirm the header row includes: {field}
|
|
48
|
+
3. Header names must match exactly.
|
|
49
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
50
|
+
""".strip()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""src/datafun_streaming/io/io_utils.py.
|
|
2
|
+
|
|
3
|
+
CSV and JSON helpers for streaming examples.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# === IMPORTS ===
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from datafun_streaming.io.errors import missing_csv_file_message
|
|
14
|
+
|
|
15
|
+
# === EXPORTS ===
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"append_csv_row",
|
|
19
|
+
"format_message_for_log",
|
|
20
|
+
"read_csv_as_lookup",
|
|
21
|
+
"read_csv_rows",
|
|
22
|
+
"row_to_json",
|
|
23
|
+
"row_from_json",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# === DEFINE HELPER FUNCTIONS ===
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def append_csv_row(path: Path, row: dict[str, Any], fieldnames: list[str]) -> None:
|
|
30
|
+
"""Append one row to a CSV file, writing the header first if needed."""
|
|
31
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
file_exists = path.exists()
|
|
33
|
+
|
|
34
|
+
with path.open(mode="a", encoding="utf-8", newline="") as file:
|
|
35
|
+
writer = csv.DictWriter(file, fieldnames=fieldnames)
|
|
36
|
+
|
|
37
|
+
if not file_exists:
|
|
38
|
+
writer.writeheader()
|
|
39
|
+
|
|
40
|
+
writer.writerow(row)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def format_message_for_log(message: dict[str, Any]) -> str:
|
|
44
|
+
"""Format one message dictionary for readable log output."""
|
|
45
|
+
lines = ["{"]
|
|
46
|
+
|
|
47
|
+
for key, value in message.items():
|
|
48
|
+
lines.append(f" {key}: {value}")
|
|
49
|
+
|
|
50
|
+
lines.append("}")
|
|
51
|
+
return "\n".join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_csv_as_lookup(
|
|
55
|
+
path: Path,
|
|
56
|
+
*,
|
|
57
|
+
key_field: str,
|
|
58
|
+
value_field: str,
|
|
59
|
+
) -> dict[str, Any]:
|
|
60
|
+
"""Read a CSV file into a key-value lookup dictionary.
|
|
61
|
+
|
|
62
|
+
Arguments:
|
|
63
|
+
path: Path to the CSV file.
|
|
64
|
+
key_field: The column to use as the dictionary key.
|
|
65
|
+
value_field: The column to use as the dictionary value.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A dict mapping each key_field value to its value_field value.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
region_lookup = read_csv_as_lookup(
|
|
72
|
+
REGIONS_CSV, key_field="region_id", value_field="tax_rate_pct"
|
|
73
|
+
)
|
|
74
|
+
tax_rate = float(region_lookup["US-MO"]) / 100.0
|
|
75
|
+
"""
|
|
76
|
+
rows = read_csv_rows(path)
|
|
77
|
+
return {row[key_field]: row[value_field] for row in rows}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_csv_rows(path: Path) -> list[dict[str, str]]:
|
|
81
|
+
"""Read a CSV file into a list of string dictionaries."""
|
|
82
|
+
if not path.exists():
|
|
83
|
+
msg = missing_csv_file_message(path=path.as_posix())
|
|
84
|
+
raise FileNotFoundError(msg)
|
|
85
|
+
|
|
86
|
+
with path.open(mode="r", encoding="utf-8", newline="") as file:
|
|
87
|
+
reader = csv.DictReader(file)
|
|
88
|
+
|
|
89
|
+
if reader.fieldnames is None:
|
|
90
|
+
msg = f"CSV file has no header row: {path.as_posix()}"
|
|
91
|
+
raise ValueError(msg)
|
|
92
|
+
|
|
93
|
+
return list(reader)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def row_to_json(row: dict[str, Any]) -> str:
|
|
97
|
+
"""Convert a row dictionary to compact JSON text."""
|
|
98
|
+
return json.dumps(row, sort_keys=True, separators=(",", ":"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def row_from_json(text: str) -> dict[str, Any]:
|
|
102
|
+
"""Convert JSON text to a row dictionary."""
|
|
103
|
+
value = json.loads(text)
|
|
104
|
+
|
|
105
|
+
if not isinstance(value, dict):
|
|
106
|
+
msg = "Expected JSON object."
|
|
107
|
+
raise ValueError(msg)
|
|
108
|
+
|
|
109
|
+
return value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Kafka producer, consumer, admin, and connection utilities."""
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""src/datafun_streaming/kafka/errors.py.
|
|
2
|
+
|
|
3
|
+
Error messages for Kafka.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# === EXPORTS
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"kafka_admin_failed_message",
|
|
10
|
+
"kafka_consume_failed_message",
|
|
11
|
+
"kafka_delivery_failed_message",
|
|
12
|
+
"kafka_no_messages_message",
|
|
13
|
+
"kafka_not_reachable_message",
|
|
14
|
+
"kafka_topic_empty_message",
|
|
15
|
+
"kafka_topic_not_found_message",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# === DEFINE HELPER FUNCTIONS ===
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def kafka_admin_failed_message(*, operation: str, topic: str, detail: str) -> str:
|
|
22
|
+
"""Return help text for a failed Kafka admin operation."""
|
|
23
|
+
return f"""
|
|
24
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
25
|
+
A Kafka admin operation failed.
|
|
26
|
+
Operation: {operation}
|
|
27
|
+
Topic: {topic}
|
|
28
|
+
Details:
|
|
29
|
+
{detail}
|
|
30
|
+
|
|
31
|
+
CHECK:
|
|
32
|
+
1. Confirm Kafka is running. Follow ref_START_KAFKA.md.
|
|
33
|
+
2. Confirm you have permission to {operation} topics.
|
|
34
|
+
3. Try the operation manually from the CLI:
|
|
35
|
+
cd ~/kafka
|
|
36
|
+
bin/kafka-topics.sh --list --bootstrap-server localhost:9092
|
|
37
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
38
|
+
""".strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def kafka_consume_failed_message(*, detail: str) -> str:
|
|
42
|
+
"""Return help text for a Kafka consume failure."""
|
|
43
|
+
return f"""
|
|
44
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
45
|
+
The consumer ran, but Kafka did not return a usable message.
|
|
46
|
+
Kafka reported an error while consuming a message.
|
|
47
|
+
Details:
|
|
48
|
+
{detail}
|
|
49
|
+
|
|
50
|
+
CHECK:
|
|
51
|
+
1. Confirm Kafka is running.
|
|
52
|
+
2. Confirm the topic exists. Follow MANAGE_TOPIC.md.
|
|
53
|
+
3. Run the producer again if the topic has no messages.
|
|
54
|
+
4. If you already consumed these messages,
|
|
55
|
+
set a different KAFKA_GROUP_ID in .env.
|
|
56
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
57
|
+
""".strip()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def kafka_delivery_failed_message(*, detail: str) -> str:
|
|
61
|
+
"""Return help text for a Kafka delivery failure."""
|
|
62
|
+
return f"""
|
|
63
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
64
|
+
The message was generated, but Kafka did not accept it.
|
|
65
|
+
Kafka did not confirm message delivery.
|
|
66
|
+
Details:
|
|
67
|
+
{detail}
|
|
68
|
+
|
|
69
|
+
CHECK:
|
|
70
|
+
1. Confirm Kafka is running.
|
|
71
|
+
2. Confirm the topic exists.
|
|
72
|
+
3. Confirm the broker is reachable at localhost:9092.
|
|
73
|
+
4. Try MANAGE_TOPIC.md to verify Kafka independently of Python.
|
|
74
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
75
|
+
""".strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def kafka_no_messages_message() -> str:
|
|
79
|
+
"""Return help text when no Kafka messages are consumed."""
|
|
80
|
+
return """
|
|
81
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
82
|
+
Kafka may be reachable, but no unread messages
|
|
83
|
+
were available for this consumer.
|
|
84
|
+
No message received before timeout.
|
|
85
|
+
|
|
86
|
+
CHECK:
|
|
87
|
+
1. Confirm Kafka is running.
|
|
88
|
+
2. Confirm the topic exists. Follow MANAGE_TOPIC.md.
|
|
89
|
+
3. Run the producer in another project terminal.
|
|
90
|
+
4. If this consumer group already read the messages,
|
|
91
|
+
set a different KAFKA_GROUP_ID in .env.
|
|
92
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
93
|
+
""".strip()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def kafka_not_reachable_message(*, bootstrap_servers: str) -> str:
|
|
97
|
+
"""Return help text for a Kafka connection failure."""
|
|
98
|
+
return f"""
|
|
99
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
100
|
+
Python code is running,
|
|
101
|
+
but Kafka is not available.
|
|
102
|
+
Kafka is not reachable.
|
|
103
|
+
The program tried to connect to:
|
|
104
|
+
KAFKA_BOOTSTRAP_SERVERS = {bootstrap_servers}
|
|
105
|
+
|
|
106
|
+
CHECK:
|
|
107
|
+
1. Start Kafka first. Follow START_KAFKA.md.
|
|
108
|
+
2. Verify Kafka is running. In a terminal, run:
|
|
109
|
+
cd ~/kafka
|
|
110
|
+
bin/kafka-topics.sh --list --bootstrap-server localhost:9092
|
|
111
|
+
3. Verify the topic exists. Follow MANAGE_TOPIC.md.
|
|
112
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
113
|
+
""".strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def kafka_topic_empty_message(*, topic: str) -> str:
|
|
117
|
+
"""Return help text when a Kafka topic exists but has no messages."""
|
|
118
|
+
return f"""
|
|
119
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
120
|
+
The topic exists but contains no messages.
|
|
121
|
+
Topic is empty:
|
|
122
|
+
KAFKA_TOPIC = {topic}
|
|
123
|
+
|
|
124
|
+
CHECK:
|
|
125
|
+
1. Run the producer first to send messages to this topic.
|
|
126
|
+
2. If you already ran the producer, confirm it completed successfully.
|
|
127
|
+
3. If messages were consumed by another consumer group,
|
|
128
|
+
run the producer again to repopulate the topic.
|
|
129
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
130
|
+
""".strip()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def kafka_topic_not_found_message(*, topic: str, bootstrap_servers: str) -> str:
|
|
134
|
+
"""Return help text when a required Kafka topic does not exist."""
|
|
135
|
+
return f"""
|
|
136
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
137
|
+
The topic does not exist in Kafka.
|
|
138
|
+
Topic not found:
|
|
139
|
+
KAFKA_TOPIC = {topic}
|
|
140
|
+
KAFKA_BOOTSTRAP_SERVERS = {bootstrap_servers}
|
|
141
|
+
|
|
142
|
+
CHECK:
|
|
143
|
+
1. Create the topic first. Follow ref_MANAGE_TOPIC.md.
|
|
144
|
+
cd ~/kafka
|
|
145
|
+
bin/kafka-topics.sh --create --topic {topic} --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
|
|
146
|
+
2. Confirm the topic was created:
|
|
147
|
+
bin/kafka-topics.sh --list --bootstrap-server localhost:9092
|
|
148
|
+
3. Confirm KAFKA_TOPIC in .env matches the topic you created.
|
|
149
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
150
|
+
""".strip()
|