moose-lib 0.6.96__tar.gz → 0.6.98__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.
Potentially problematic release.
This version of moose-lib might be problematic. Click here for more details.
- {moose_lib-0.6.96 → moose_lib-0.6.98}/PKG-INFO +1 -1
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/commons.py +83 -1
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/config/config_file.py +37 -1
- moose_lib-0.6.98/moose_lib/config/runtime.py +198 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/stream.py +116 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/streaming/streaming_function_runner.py +25 -39
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib.egg-info/PKG-INFO +1 -1
- moose_lib-0.6.96/moose_lib/config/runtime.py +0 -110
- {moose_lib-0.6.96 → moose_lib-0.6.98}/README.md +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/blocks.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/clients/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/clients/redis_client.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/config/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/data_models.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/_registry.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/consumption.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/ingest_api.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/ingest_pipeline.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/life_cycle.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/materialized_view.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/olap_table.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/registry.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/sql_resource.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/types.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/view.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2/workflow.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/dmv2_serializer.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/internal.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/main.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/query_builder.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/query_param.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/streaming/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/utilities/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib/utilities/sql.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib.egg-info/SOURCES.txt +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib.egg-info/dependency_links.txt +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib.egg-info/requires.txt +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/moose_lib.egg-info/top_level.txt +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/setup.cfg +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/setup.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/tests/__init__.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/tests/conftest.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/tests/test_moose.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/tests/test_query_builder.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/tests/test_redis_client.py +0 -0
- {moose_lib-0.6.96 → moose_lib-0.6.98}/tests/test_s3queue_config.py +0 -0
|
@@ -4,8 +4,9 @@ from datetime import datetime, timezone
|
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
import json
|
|
7
|
-
from typing import Optional, Literal
|
|
7
|
+
from typing import Optional, Literal, Any, Union, Callable
|
|
8
8
|
import os
|
|
9
|
+
from kafka import KafkaConsumer, KafkaProducer
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class CliLogData:
|
|
@@ -100,3 +101,84 @@ class EnhancedJSONEncoder(json.JSONEncoder):
|
|
|
100
101
|
if dataclasses.is_dataclass(o):
|
|
101
102
|
return dataclasses.asdict(o)
|
|
102
103
|
return super().default(o)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_kafka_kwargs(
|
|
107
|
+
broker: Union[str, list[str]],
|
|
108
|
+
sasl_username: Optional[str] = None,
|
|
109
|
+
sasl_password: Optional[str] = None,
|
|
110
|
+
sasl_mechanism: Optional[str] = None,
|
|
111
|
+
security_protocol: Optional[str] = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""Builds common Kafka client kwargs from provided parameters."""
|
|
114
|
+
kwargs: dict[str, Any] = {
|
|
115
|
+
"bootstrap_servers": broker,
|
|
116
|
+
}
|
|
117
|
+
if sasl_mechanism:
|
|
118
|
+
kwargs["sasl_mechanism"] = sasl_mechanism
|
|
119
|
+
if sasl_username is not None:
|
|
120
|
+
kwargs["sasl_plain_username"] = sasl_username
|
|
121
|
+
if sasl_password is not None:
|
|
122
|
+
kwargs["sasl_plain_password"] = sasl_password
|
|
123
|
+
if security_protocol is not None:
|
|
124
|
+
kwargs["security_protocol"] = security_protocol
|
|
125
|
+
return kwargs
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_kafka_consumer(
|
|
129
|
+
*,
|
|
130
|
+
broker: Union[str, list[str]],
|
|
131
|
+
client_id: str,
|
|
132
|
+
group_id: str,
|
|
133
|
+
sasl_username: Optional[str] = None,
|
|
134
|
+
sasl_password: Optional[str] = None,
|
|
135
|
+
sasl_mechanism: Optional[str] = None,
|
|
136
|
+
security_protocol: Optional[str] = None,
|
|
137
|
+
value_deserializer=lambda m: json.loads(m.decode("utf-8")),
|
|
138
|
+
**extra_kwargs: Any,
|
|
139
|
+
) -> KafkaConsumer:
|
|
140
|
+
"""Creates a configured KafkaConsumer with optional SASL/security settings."""
|
|
141
|
+
kwargs = _build_kafka_kwargs(
|
|
142
|
+
broker,
|
|
143
|
+
sasl_username=sasl_username,
|
|
144
|
+
sasl_password=sasl_password,
|
|
145
|
+
sasl_mechanism=sasl_mechanism,
|
|
146
|
+
security_protocol=security_protocol,
|
|
147
|
+
)
|
|
148
|
+
return KafkaConsumer(
|
|
149
|
+
client_id=client_id,
|
|
150
|
+
group_id=group_id,
|
|
151
|
+
value_deserializer=value_deserializer,
|
|
152
|
+
**kwargs,
|
|
153
|
+
**extra_kwargs,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_kafka_producer(
|
|
158
|
+
*,
|
|
159
|
+
broker: Union[str, list[str]],
|
|
160
|
+
sasl_username: Optional[str] = None,
|
|
161
|
+
sasl_password: Optional[str] = None,
|
|
162
|
+
sasl_mechanism: Optional[str] = None,
|
|
163
|
+
security_protocol: Optional[str] = None,
|
|
164
|
+
max_request_size: Optional[int] = None,
|
|
165
|
+
value_serializer: Optional[Callable[[Any], bytes]] = None,
|
|
166
|
+
**extra_kwargs: Any,
|
|
167
|
+
) -> KafkaProducer:
|
|
168
|
+
"""Creates a configured KafkaProducer with optional SASL/security settings.
|
|
169
|
+
"""
|
|
170
|
+
kwargs = _build_kafka_kwargs(
|
|
171
|
+
broker,
|
|
172
|
+
sasl_username=sasl_username,
|
|
173
|
+
sasl_password=sasl_password,
|
|
174
|
+
sasl_mechanism=sasl_mechanism,
|
|
175
|
+
security_protocol=security_protocol,
|
|
176
|
+
)
|
|
177
|
+
if max_request_size is not None:
|
|
178
|
+
kwargs["max_request_size"] = max_request_size
|
|
179
|
+
kwargs["max_in_flight_requests_per_connection"] = 1
|
|
180
|
+
if value_serializer is not None:
|
|
181
|
+
kwargs["value_serializer"] = value_serializer
|
|
182
|
+
# Allow callers to pass through additional Kafka configs like linger_ms, acks, retries, etc.
|
|
183
|
+
kwargs.update(extra_kwargs)
|
|
184
|
+
return KafkaProducer(**kwargs)
|
|
@@ -9,6 +9,7 @@ import tomllib
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
@dataclass
|
|
13
14
|
class ClickHouseConfig:
|
|
14
15
|
"""ClickHouse configuration settings from moose.config.toml."""
|
|
@@ -20,11 +21,26 @@ class ClickHouseConfig:
|
|
|
20
21
|
use_ssl: bool = False
|
|
21
22
|
native_port: Optional[int] = None
|
|
22
23
|
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class KafkaConfig:
|
|
27
|
+
"""Redpanda/Kafka configuration settings from moose.config.toml."""
|
|
28
|
+
broker: str
|
|
29
|
+
message_timeout_ms: int
|
|
30
|
+
sasl_username: Optional[str] = None
|
|
31
|
+
sasl_password: Optional[str] = None
|
|
32
|
+
sasl_mechanism: Optional[str] = None
|
|
33
|
+
security_protocol: Optional[str] = None
|
|
34
|
+
namespace: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
23
37
|
@dataclass
|
|
24
38
|
class ProjectConfig:
|
|
25
39
|
"""Project configuration from moose.config.toml."""
|
|
26
40
|
language: str
|
|
27
41
|
clickhouse_config: ClickHouseConfig
|
|
42
|
+
kafka_config: Optional[KafkaConfig] = None
|
|
43
|
+
|
|
28
44
|
|
|
29
45
|
def find_config_file(start_dir: str = os.getcwd()) -> Optional[str]:
|
|
30
46
|
"""Find moose.config.toml by walking up directory tree.
|
|
@@ -48,6 +64,7 @@ def find_config_file(start_dir: str = os.getcwd()) -> Optional[str]:
|
|
|
48
64
|
current_dir = parent_dir
|
|
49
65
|
return None
|
|
50
66
|
|
|
67
|
+
|
|
51
68
|
def read_project_config() -> ProjectConfig:
|
|
52
69
|
"""Read and parse moose.config.toml.
|
|
53
70
|
|
|
@@ -74,9 +91,28 @@ def read_project_config() -> ProjectConfig:
|
|
|
74
91
|
native_port=config_data["clickhouse_config"].get("native_port")
|
|
75
92
|
)
|
|
76
93
|
|
|
94
|
+
def _parse_kafka(section_name: str) -> Optional[KafkaConfig]:
|
|
95
|
+
sec = config_data.get(section_name)
|
|
96
|
+
if sec is None:
|
|
97
|
+
return None
|
|
98
|
+
return KafkaConfig(
|
|
99
|
+
broker=sec["broker"],
|
|
100
|
+
message_timeout_ms=sec.get("message_timeout_ms", 1000),
|
|
101
|
+
sasl_username=sec.get("sasl_username"),
|
|
102
|
+
sasl_password=sec.get("sasl_password"),
|
|
103
|
+
sasl_mechanism=sec.get("sasl_mechanism"),
|
|
104
|
+
security_protocol=sec.get("security_protocol"),
|
|
105
|
+
namespace=sec.get("namespace"),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
kafka_cfg = _parse_kafka("kafka_config")
|
|
109
|
+
if kafka_cfg is None:
|
|
110
|
+
kafka_cfg = _parse_kafka("redpanda_config")
|
|
111
|
+
|
|
77
112
|
return ProjectConfig(
|
|
78
113
|
language=config_data["language"],
|
|
79
|
-
clickhouse_config=clickhouse_config
|
|
114
|
+
clickhouse_config=clickhouse_config,
|
|
115
|
+
kafka_config=kafka_cfg,
|
|
80
116
|
)
|
|
81
117
|
except Exception as e:
|
|
82
118
|
raise RuntimeError(f"Failed to parse moose.config.toml: {e}")
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime configuration management for Moose.
|
|
3
|
+
|
|
4
|
+
This module provides a singleton registry for managing runtime configuration settings,
|
|
5
|
+
particularly for ClickHouse connections.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RuntimeClickHouseConfig:
|
|
14
|
+
"""Runtime ClickHouse configuration settings."""
|
|
15
|
+
host: str
|
|
16
|
+
port: str
|
|
17
|
+
username: str
|
|
18
|
+
password: str
|
|
19
|
+
database: str
|
|
20
|
+
use_ssl: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class RuntimeKafkaConfig:
|
|
25
|
+
"""Runtime Kafka configuration settings."""
|
|
26
|
+
broker: str
|
|
27
|
+
message_timeout_ms: int
|
|
28
|
+
sasl_username: Optional[str]
|
|
29
|
+
sasl_password: Optional[str]
|
|
30
|
+
sasl_mechanism: Optional[str]
|
|
31
|
+
security_protocol: Optional[str]
|
|
32
|
+
namespace: Optional[str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConfigurationRegistry:
|
|
36
|
+
"""Singleton registry for managing runtime configuration.
|
|
37
|
+
|
|
38
|
+
This class provides a centralized way to manage and access runtime configuration
|
|
39
|
+
settings, with fallback to file-based configuration when runtime settings are not set.
|
|
40
|
+
"""
|
|
41
|
+
_instance: Optional['ConfigurationRegistry'] = None
|
|
42
|
+
_clickhouse_config: Optional[RuntimeClickHouseConfig] = None
|
|
43
|
+
_kafka_config: Optional[RuntimeKafkaConfig] = None
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def get_instance(cls) -> 'ConfigurationRegistry':
|
|
47
|
+
"""Get the singleton instance of ConfigurationRegistry.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The singleton ConfigurationRegistry instance.
|
|
51
|
+
"""
|
|
52
|
+
if not cls._instance:
|
|
53
|
+
cls._instance = cls()
|
|
54
|
+
return cls._instance
|
|
55
|
+
|
|
56
|
+
def set_clickhouse_config(self, config: RuntimeClickHouseConfig) -> None:
|
|
57
|
+
"""Set the runtime ClickHouse configuration.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
config: The ClickHouse configuration to use.
|
|
61
|
+
"""
|
|
62
|
+
self._clickhouse_config = config
|
|
63
|
+
|
|
64
|
+
def set_kafka_config(self, config: 'RuntimeKafkaConfig') -> None:
|
|
65
|
+
"""Set the runtime Kafka configuration.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
config: The Kafka configuration to use.
|
|
69
|
+
"""
|
|
70
|
+
self._kafka_config = config
|
|
71
|
+
|
|
72
|
+
def get_clickhouse_config(self) -> RuntimeClickHouseConfig:
|
|
73
|
+
"""Get the current ClickHouse configuration.
|
|
74
|
+
|
|
75
|
+
If runtime configuration is not set, falls back to reading from moose.config.toml.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The current ClickHouse configuration.
|
|
79
|
+
"""
|
|
80
|
+
if self._clickhouse_config:
|
|
81
|
+
return self._clickhouse_config
|
|
82
|
+
|
|
83
|
+
# Fallback to reading from config file
|
|
84
|
+
from .config_file import read_project_config
|
|
85
|
+
|
|
86
|
+
def _env(name: str) -> Optional[str]:
|
|
87
|
+
val = os.environ.get(name)
|
|
88
|
+
if val is None:
|
|
89
|
+
return None
|
|
90
|
+
trimmed = val.strip()
|
|
91
|
+
return trimmed if trimmed else None
|
|
92
|
+
|
|
93
|
+
def _parse_bool(val: Optional[str]) -> Optional[bool]:
|
|
94
|
+
if val is None:
|
|
95
|
+
return None
|
|
96
|
+
v = val.strip().lower()
|
|
97
|
+
if v in ("1", "true", "yes", "on"):
|
|
98
|
+
return True
|
|
99
|
+
if v in ("0", "false", "no", "off"):
|
|
100
|
+
return False
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
config = read_project_config()
|
|
105
|
+
|
|
106
|
+
env_host = _env("MOOSE_CLICKHOUSE_CONFIG__HOST")
|
|
107
|
+
env_port = _env("MOOSE_CLICKHOUSE_CONFIG__HOST_PORT")
|
|
108
|
+
env_user = _env("MOOSE_CLICKHOUSE_CONFIG__USER")
|
|
109
|
+
env_password = _env("MOOSE_CLICKHOUSE_CONFIG__PASSWORD")
|
|
110
|
+
env_db = _env("MOOSE_CLICKHOUSE_CONFIG__DB_NAME")
|
|
111
|
+
env_use_ssl = _parse_bool(_env("MOOSE_CLICKHOUSE_CONFIG__USE_SSL"))
|
|
112
|
+
|
|
113
|
+
return RuntimeClickHouseConfig(
|
|
114
|
+
host=env_host or config.clickhouse_config.host,
|
|
115
|
+
port=(env_port or str(config.clickhouse_config.host_port)),
|
|
116
|
+
username=env_user or config.clickhouse_config.user,
|
|
117
|
+
password=env_password or config.clickhouse_config.password,
|
|
118
|
+
database=env_db or config.clickhouse_config.db_name,
|
|
119
|
+
use_ssl=(env_use_ssl if env_use_ssl is not None else config.clickhouse_config.use_ssl),
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise RuntimeError(f"Failed to get ClickHouse configuration: {e}")
|
|
123
|
+
|
|
124
|
+
def get_kafka_config(self) -> 'RuntimeKafkaConfig':
|
|
125
|
+
"""Get the current Kafka configuration.
|
|
126
|
+
|
|
127
|
+
If runtime configuration is not set, falls back to reading from moose.config.toml
|
|
128
|
+
and environment variables (Redpanda- and Kafka-prefixed).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The current Kafka configuration.
|
|
132
|
+
"""
|
|
133
|
+
if self._kafka_config:
|
|
134
|
+
return self._kafka_config
|
|
135
|
+
|
|
136
|
+
from .config_file import read_project_config
|
|
137
|
+
|
|
138
|
+
def _env(name: str) -> Optional[str]:
|
|
139
|
+
val = os.environ.get(name)
|
|
140
|
+
if val is None:
|
|
141
|
+
return None
|
|
142
|
+
trimmed = val.strip()
|
|
143
|
+
return trimmed if trimmed else None
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
config = read_project_config()
|
|
147
|
+
|
|
148
|
+
# Prefer Redpanda-prefixed env vars; fallback to Kafka-prefixed
|
|
149
|
+
broker = _env("MOOSE_REDPANDA_CONFIG__BROKER") or \
|
|
150
|
+
_env("MOOSE_KAFKA_CONFIG__BROKER")
|
|
151
|
+
message_timeout_ms = _env("MOOSE_REDPANDA_CONFIG__MESSAGE_TIMEOUT_MS") or \
|
|
152
|
+
_env("MOOSE_KAFKA_CONFIG__MESSAGE_TIMEOUT_MS")
|
|
153
|
+
sasl_username = _env("MOOSE_REDPANDA_CONFIG__SASL_USERNAME") or \
|
|
154
|
+
_env("MOOSE_KAFKA_CONFIG__SASL_USERNAME")
|
|
155
|
+
sasl_password = _env("MOOSE_REDPANDA_CONFIG__SASL_PASSWORD") or \
|
|
156
|
+
_env("MOOSE_KAFKA_CONFIG__SASL_PASSWORD")
|
|
157
|
+
sasl_mechanism = _env("MOOSE_REDPANDA_CONFIG__SASL_MECHANISM") or \
|
|
158
|
+
_env("MOOSE_KAFKA_CONFIG__SASL_MECHANISM")
|
|
159
|
+
security_protocol = _env("MOOSE_REDPANDA_CONFIG__SECURITY_PROTOCOL") or \
|
|
160
|
+
_env("MOOSE_KAFKA_CONFIG__SECURITY_PROTOCOL")
|
|
161
|
+
namespace = _env("MOOSE_REDPANDA_CONFIG__NAMESPACE") or \
|
|
162
|
+
_env("MOOSE_KAFKA_CONFIG__NAMESPACE")
|
|
163
|
+
|
|
164
|
+
file_kafka = config.kafka_config
|
|
165
|
+
|
|
166
|
+
def _to_int(value: Optional[str], fallback: int) -> int:
|
|
167
|
+
try:
|
|
168
|
+
return int(value) if value is not None else fallback
|
|
169
|
+
except Exception:
|
|
170
|
+
return fallback
|
|
171
|
+
|
|
172
|
+
return RuntimeKafkaConfig(
|
|
173
|
+
broker=broker or (file_kafka.broker if file_kafka else "localhost:19092"),
|
|
174
|
+
message_timeout_ms=_to_int(message_timeout_ms, file_kafka.message_timeout_ms if file_kafka else 1000),
|
|
175
|
+
sasl_username=sasl_username if sasl_username is not None else (
|
|
176
|
+
file_kafka.sasl_username if file_kafka else None),
|
|
177
|
+
sasl_password=sasl_password if sasl_password is not None else (
|
|
178
|
+
file_kafka.sasl_password if file_kafka else None),
|
|
179
|
+
sasl_mechanism=sasl_mechanism if sasl_mechanism is not None else (
|
|
180
|
+
file_kafka.sasl_mechanism if file_kafka else None),
|
|
181
|
+
security_protocol=security_protocol if security_protocol is not None else (
|
|
182
|
+
file_kafka.security_protocol if file_kafka else None),
|
|
183
|
+
namespace=namespace if namespace is not None else (file_kafka.namespace if file_kafka else None),
|
|
184
|
+
)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise RuntimeError(f"Failed to get Kafka configuration: {e}")
|
|
187
|
+
|
|
188
|
+
def has_runtime_config(self) -> bool:
|
|
189
|
+
"""Check if runtime configuration is set.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if either runtime clickhouse or kafka configuration is set, False otherwise.
|
|
193
|
+
"""
|
|
194
|
+
return self._clickhouse_config is not None or self._kafka_config is not None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Create singleton instance
|
|
198
|
+
config_registry = ConfigurationRegistry.get_instance()
|
|
@@ -6,14 +6,18 @@ including stream transformations, consumers, and dead letter queues.
|
|
|
6
6
|
"""
|
|
7
7
|
import dataclasses
|
|
8
8
|
import datetime
|
|
9
|
+
import json
|
|
9
10
|
from typing import Any, Optional, Callable, Union, Literal, Generic
|
|
10
11
|
from pydantic import BaseModel, ConfigDict, AliasGenerator
|
|
11
12
|
from pydantic.alias_generators import to_camel
|
|
13
|
+
from kafka import KafkaProducer
|
|
12
14
|
|
|
13
15
|
from .types import TypedMooseResource, ZeroOrMany, T, U
|
|
14
16
|
from .olap_table import OlapTable
|
|
15
17
|
from ._registry import _streams
|
|
16
18
|
from .life_cycle import LifeCycle
|
|
19
|
+
from ..config.runtime import config_registry, RuntimeKafkaConfig
|
|
20
|
+
from ..commons import get_kafka_producer
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class StreamConfig(BaseModel):
|
|
@@ -110,6 +114,8 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
110
114
|
consumers: list[ConsumerEntry[T]]
|
|
111
115
|
_multipleTransformations: Optional[Callable[[T], list[_RoutedMessage]]] = None
|
|
112
116
|
default_dead_letter_queue: "Optional[DeadLetterQueue[T]]" = None
|
|
117
|
+
_memoized_producer: Optional[KafkaProducer] = None
|
|
118
|
+
_kafka_config_hash: Optional[str] = None
|
|
113
119
|
|
|
114
120
|
def __init__(self, name: str, config: "StreamConfig" = None, **kwargs):
|
|
115
121
|
super().__init__()
|
|
@@ -216,6 +222,116 @@ class Stream(TypedMooseResource, Generic[T]):
|
|
|
216
222
|
"""
|
|
217
223
|
self._multipleTransformations = transformation
|
|
218
224
|
|
|
225
|
+
def _build_full_topic_name(self, namespace: Optional[str]) -> str:
|
|
226
|
+
"""Build full topic name with optional namespace and version suffix."""
|
|
227
|
+
version_suffix = f"_{self.config.version.replace('.', '_')}" if self.config.version else ""
|
|
228
|
+
base = f"{self.name}{version_suffix}"
|
|
229
|
+
return f"{namespace}.{base}" if namespace else base
|
|
230
|
+
|
|
231
|
+
def _create_kafka_config_hash(self, cfg: RuntimeKafkaConfig) -> str:
|
|
232
|
+
import hashlib
|
|
233
|
+
config_string = ":".join(
|
|
234
|
+
str(x)
|
|
235
|
+
for x in (
|
|
236
|
+
cfg.broker,
|
|
237
|
+
cfg.message_timeout_ms,
|
|
238
|
+
cfg.sasl_username,
|
|
239
|
+
cfg.sasl_password,
|
|
240
|
+
cfg.sasl_mechanism,
|
|
241
|
+
cfg.security_protocol,
|
|
242
|
+
cfg.namespace,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
return hashlib.sha256(config_string.encode()).hexdigest()[:16]
|
|
246
|
+
|
|
247
|
+
def _parse_brokers(self, broker_string: str) -> list[str]:
|
|
248
|
+
if not broker_string:
|
|
249
|
+
return []
|
|
250
|
+
return [b.strip() for b in broker_string.split(",") if b.strip()]
|
|
251
|
+
|
|
252
|
+
def _get_memoized_producer(self) -> tuple[KafkaProducer, Any]:
|
|
253
|
+
"""Create or reuse a KafkaProducer using runtime configuration."""
|
|
254
|
+
cfg = config_registry.get_kafka_config()
|
|
255
|
+
current_hash = self._create_kafka_config_hash(cfg)
|
|
256
|
+
|
|
257
|
+
if self._memoized_producer is not None and self._kafka_config_hash == current_hash:
|
|
258
|
+
return self._memoized_producer, cfg
|
|
259
|
+
|
|
260
|
+
# Close previous producer if config changed
|
|
261
|
+
if self._memoized_producer is not None and self._kafka_config_hash != current_hash:
|
|
262
|
+
try:
|
|
263
|
+
self._memoized_producer.flush()
|
|
264
|
+
self._memoized_producer.close()
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
finally:
|
|
268
|
+
self._memoized_producer = None
|
|
269
|
+
|
|
270
|
+
brokers = self._parse_brokers(getattr(cfg, "broker", "localhost:19092"))
|
|
271
|
+
if not brokers:
|
|
272
|
+
raise RuntimeError(f"No valid broker addresses found in: '{getattr(cfg, 'broker', '')}'")
|
|
273
|
+
|
|
274
|
+
producer = get_kafka_producer(
|
|
275
|
+
broker=brokers,
|
|
276
|
+
sasl_username=getattr(cfg, "sasl_username", None),
|
|
277
|
+
sasl_password=getattr(cfg, "sasl_password", None),
|
|
278
|
+
sasl_mechanism=getattr(cfg, "sasl_mechanism", None),
|
|
279
|
+
security_protocol=getattr(cfg, "security_protocol", None),
|
|
280
|
+
value_serializer=lambda v: v.model_dump_json().encode('utf-8'),
|
|
281
|
+
acks="all",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
self._memoized_producer = producer
|
|
285
|
+
self._kafka_config_hash = current_hash
|
|
286
|
+
return producer, cfg
|
|
287
|
+
|
|
288
|
+
def close_producer(self) -> None:
|
|
289
|
+
"""Closes the memoized Kafka producer if it exists."""
|
|
290
|
+
if self._memoized_producer is not None:
|
|
291
|
+
try:
|
|
292
|
+
self._memoized_producer.flush()
|
|
293
|
+
self._memoized_producer.close()
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
finally:
|
|
297
|
+
self._memoized_producer = None
|
|
298
|
+
self._kafka_config_hash = None
|
|
299
|
+
|
|
300
|
+
def send(self, values: ZeroOrMany[T]) -> None:
|
|
301
|
+
"""Send one or more records to this stream's Kafka topic.
|
|
302
|
+
|
|
303
|
+
Values are JSON-serialized using EnhancedJSONEncoder.
|
|
304
|
+
"""
|
|
305
|
+
# Normalize inputs to a flat list of records
|
|
306
|
+
filtered: list[T] = []
|
|
307
|
+
if isinstance(values, list):
|
|
308
|
+
for v in values:
|
|
309
|
+
if v is None:
|
|
310
|
+
continue
|
|
311
|
+
else:
|
|
312
|
+
filtered.append(v)
|
|
313
|
+
elif values is not None:
|
|
314
|
+
filtered.append(values) # type: ignore[arg-type]
|
|
315
|
+
|
|
316
|
+
if len(filtered) == 0:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
# ensure all records are instances of the stream's model type
|
|
320
|
+
model_type = self._t
|
|
321
|
+
for rec in filtered:
|
|
322
|
+
if not isinstance(rec, model_type):
|
|
323
|
+
raise TypeError(
|
|
324
|
+
f"Stream '{self.name}' expects instances of {model_type.__name__}, "
|
|
325
|
+
f"got {type(rec).__name__}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
producer, cfg = self._get_memoized_producer()
|
|
329
|
+
topic = self._build_full_topic_name(getattr(cfg, "namespace", None))
|
|
330
|
+
|
|
331
|
+
for rec in filtered:
|
|
332
|
+
producer.send(topic, value=rec)
|
|
333
|
+
producer.flush()
|
|
334
|
+
|
|
219
335
|
|
|
220
336
|
class DeadLetterModel(BaseModel, Generic[T]):
|
|
221
337
|
"""Model for dead letter queue messages.
|
|
@@ -30,7 +30,12 @@ from typing import Optional, Callable, Tuple, Any
|
|
|
30
30
|
|
|
31
31
|
from moose_lib.dmv2 import get_streams, DeadLetterModel
|
|
32
32
|
from moose_lib import cli_log, CliLogData, DeadLetterQueue
|
|
33
|
-
from moose_lib.commons import
|
|
33
|
+
from moose_lib.commons import (
|
|
34
|
+
EnhancedJSONEncoder,
|
|
35
|
+
moose_management_port,
|
|
36
|
+
get_kafka_consumer,
|
|
37
|
+
get_kafka_producer,
|
|
38
|
+
)
|
|
34
39
|
|
|
35
40
|
# Force stdout to be unbuffered
|
|
36
41
|
sys.stdout = io.TextIOWrapper(
|
|
@@ -294,29 +299,18 @@ def create_consumer() -> KafkaConsumer:
|
|
|
294
299
|
Returns:
|
|
295
300
|
Configured KafkaConsumer instance
|
|
296
301
|
"""
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
else:
|
|
311
|
-
log("No sasl mechanism specified. Using default consumer.")
|
|
312
|
-
return KafkaConsumer(
|
|
313
|
-
source_topic.name,
|
|
314
|
-
client_id="python_streaming_function_consumer",
|
|
315
|
-
group_id=streaming_function_id,
|
|
316
|
-
bootstrap_servers=broker,
|
|
317
|
-
# consumer_timeout_ms=10000,
|
|
318
|
-
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
|
|
319
|
-
)
|
|
302
|
+
kwargs = dict(
|
|
303
|
+
broker=broker,
|
|
304
|
+
client_id="python_streaming_function_consumer",
|
|
305
|
+
group_id=streaming_function_id,
|
|
306
|
+
value_deserializer=lambda m: json.loads(m.decode("utf-8")),
|
|
307
|
+
sasl_username=sasl_config.get("username"),
|
|
308
|
+
sasl_password=sasl_config.get("password"),
|
|
309
|
+
sasl_mechanism=sasl_config.get("mechanism"),
|
|
310
|
+
security_protocol=args.security_protocol,
|
|
311
|
+
)
|
|
312
|
+
consumer = get_kafka_consumer(**kwargs)
|
|
313
|
+
return consumer
|
|
320
314
|
|
|
321
315
|
|
|
322
316
|
def create_producer() -> Optional[KafkaProducer]:
|
|
@@ -330,20 +324,13 @@ def create_producer() -> Optional[KafkaProducer]:
|
|
|
330
324
|
"""
|
|
331
325
|
max_request_size = KafkaProducer.DEFAULT_CONFIG['max_request_size'] if target_topic is None \
|
|
332
326
|
else target_topic.max_message_bytes
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
max_request_size=max_request_size
|
|
341
|
-
)
|
|
342
|
-
log("No sasl mechanism specified. Using default producer.")
|
|
343
|
-
return KafkaProducer(
|
|
344
|
-
bootstrap_servers=broker,
|
|
345
|
-
max_in_flight_requests_per_connection=1,
|
|
346
|
-
max_request_size=max_request_size
|
|
327
|
+
return get_kafka_producer(
|
|
328
|
+
broker=broker,
|
|
329
|
+
sasl_username=sasl_config.get("username"),
|
|
330
|
+
sasl_password=sasl_config.get("password"),
|
|
331
|
+
sasl_mechanism=sasl_config.get("mechanism"),
|
|
332
|
+
security_protocol=args.security_protocol,
|
|
333
|
+
max_request_size=max_request_size,
|
|
347
334
|
)
|
|
348
335
|
|
|
349
336
|
|
|
@@ -416,7 +403,6 @@ def main():
|
|
|
416
403
|
kafka_refs['consumer'] = consumer
|
|
417
404
|
kafka_refs['producer'] = producer
|
|
418
405
|
|
|
419
|
-
# Subscribe to topic
|
|
420
406
|
consumer.subscribe([source_topic.name])
|
|
421
407
|
|
|
422
408
|
log("Kafka consumer and producer initialized in processing thread")
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Runtime configuration management for Moose.
|
|
3
|
-
|
|
4
|
-
This module provides a singleton registry for managing runtime configuration settings,
|
|
5
|
-
particularly for ClickHouse connections.
|
|
6
|
-
"""
|
|
7
|
-
import os
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from typing import Optional
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class RuntimeClickHouseConfig:
|
|
13
|
-
"""Runtime ClickHouse configuration settings."""
|
|
14
|
-
host: str
|
|
15
|
-
port: str
|
|
16
|
-
username: str
|
|
17
|
-
password: str
|
|
18
|
-
database: str
|
|
19
|
-
use_ssl: bool
|
|
20
|
-
|
|
21
|
-
class ConfigurationRegistry:
|
|
22
|
-
"""Singleton registry for managing runtime configuration.
|
|
23
|
-
|
|
24
|
-
This class provides a centralized way to manage and access runtime configuration
|
|
25
|
-
settings, with fallback to file-based configuration when runtime settings are not set.
|
|
26
|
-
"""
|
|
27
|
-
_instance: Optional['ConfigurationRegistry'] = None
|
|
28
|
-
_clickhouse_config: Optional[RuntimeClickHouseConfig] = None
|
|
29
|
-
|
|
30
|
-
@classmethod
|
|
31
|
-
def get_instance(cls) -> 'ConfigurationRegistry':
|
|
32
|
-
"""Get the singleton instance of ConfigurationRegistry.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
The singleton ConfigurationRegistry instance.
|
|
36
|
-
"""
|
|
37
|
-
if not cls._instance:
|
|
38
|
-
cls._instance = cls()
|
|
39
|
-
return cls._instance
|
|
40
|
-
|
|
41
|
-
def set_clickhouse_config(self, config: RuntimeClickHouseConfig) -> None:
|
|
42
|
-
"""Set the runtime ClickHouse configuration.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
config: The ClickHouse configuration to use.
|
|
46
|
-
"""
|
|
47
|
-
self._clickhouse_config = config
|
|
48
|
-
|
|
49
|
-
def get_clickhouse_config(self) -> RuntimeClickHouseConfig:
|
|
50
|
-
"""Get the current ClickHouse configuration.
|
|
51
|
-
|
|
52
|
-
If runtime configuration is not set, falls back to reading from moose.config.toml.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
The current ClickHouse configuration.
|
|
56
|
-
"""
|
|
57
|
-
if self._clickhouse_config:
|
|
58
|
-
return self._clickhouse_config
|
|
59
|
-
|
|
60
|
-
# Fallback to reading from config file
|
|
61
|
-
from .config_file import read_project_config
|
|
62
|
-
|
|
63
|
-
def _env(name: str) -> Optional[str]:
|
|
64
|
-
val = os.environ.get(name)
|
|
65
|
-
if val is None:
|
|
66
|
-
return None
|
|
67
|
-
trimmed = val.strip()
|
|
68
|
-
return trimmed if trimmed else None
|
|
69
|
-
|
|
70
|
-
def _parse_bool(val: Optional[str]) -> Optional[bool]:
|
|
71
|
-
if val is None:
|
|
72
|
-
return None
|
|
73
|
-
v = val.strip().lower()
|
|
74
|
-
if v in ("1", "true", "yes", "on"):
|
|
75
|
-
return True
|
|
76
|
-
if v in ("0", "false", "no", "off"):
|
|
77
|
-
return False
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
config = read_project_config()
|
|
82
|
-
|
|
83
|
-
env_host = _env("MOOSE_CLICKHOUSE_CONFIG__HOST")
|
|
84
|
-
env_port = _env("MOOSE_CLICKHOUSE_CONFIG__HOST_PORT")
|
|
85
|
-
env_user = _env("MOOSE_CLICKHOUSE_CONFIG__USER")
|
|
86
|
-
env_password = _env("MOOSE_CLICKHOUSE_CONFIG__PASSWORD")
|
|
87
|
-
env_db = _env("MOOSE_CLICKHOUSE_CONFIG__DB_NAME")
|
|
88
|
-
env_use_ssl = _parse_bool(_env("MOOSE_CLICKHOUSE_CONFIG__USE_SSL"))
|
|
89
|
-
|
|
90
|
-
return RuntimeClickHouseConfig(
|
|
91
|
-
host=env_host or config.clickhouse_config.host,
|
|
92
|
-
port=(env_port or str(config.clickhouse_config.host_port)),
|
|
93
|
-
username=env_user or config.clickhouse_config.user,
|
|
94
|
-
password=env_password or config.clickhouse_config.password,
|
|
95
|
-
database=env_db or config.clickhouse_config.db_name,
|
|
96
|
-
use_ssl=(env_use_ssl if env_use_ssl is not None else config.clickhouse_config.use_ssl),
|
|
97
|
-
)
|
|
98
|
-
except Exception as e:
|
|
99
|
-
raise RuntimeError(f"Failed to get ClickHouse configuration: {e}")
|
|
100
|
-
|
|
101
|
-
def has_runtime_config(self) -> bool:
|
|
102
|
-
"""Check if runtime configuration is set.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
True if runtime configuration is set, False otherwise.
|
|
106
|
-
"""
|
|
107
|
-
return self._clickhouse_config is not None
|
|
108
|
-
|
|
109
|
-
# Create singleton instance
|
|
110
|
-
config_registry = ConfigurationRegistry.get_instance()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|