moose-lib 0.6.110__tar.gz → 0.6.111__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.

Files changed (47) hide show
  1. {moose_lib-0.6.110 → moose_lib-0.6.111}/PKG-INFO +2 -1
  2. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/config/config_file.py +2 -0
  3. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/config/runtime.py +5 -0
  4. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/__init__.py +8 -0
  5. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/stream.py +66 -14
  6. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/internal.py +3 -0
  7. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/streaming/streaming_function_runner.py +9 -1
  8. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib.egg-info/PKG-INFO +2 -1
  9. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib.egg-info/requires.txt +1 -0
  10. {moose_lib-0.6.110 → moose_lib-0.6.111}/setup.py +1 -0
  11. {moose_lib-0.6.110 → moose_lib-0.6.111}/README.md +0 -0
  12. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/__init__.py +0 -0
  13. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/blocks.py +0 -0
  14. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/clients/__init__.py +0 -0
  15. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/clients/redis_client.py +0 -0
  16. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/commons.py +0 -0
  17. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/config/__init__.py +0 -0
  18. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/data_models.py +0 -0
  19. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/_registry.py +0 -0
  20. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/consumption.py +0 -0
  21. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/ingest_api.py +0 -0
  22. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/ingest_pipeline.py +0 -0
  23. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/life_cycle.py +0 -0
  24. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/materialized_view.py +0 -0
  25. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/olap_table.py +0 -0
  26. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/registry.py +0 -0
  27. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/sql_resource.py +0 -0
  28. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/types.py +0 -0
  29. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/view.py +0 -0
  30. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2/workflow.py +0 -0
  31. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/dmv2_serializer.py +0 -0
  32. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/main.py +0 -0
  33. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/query_builder.py +0 -0
  34. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/query_param.py +0 -0
  35. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/streaming/__init__.py +0 -0
  36. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/utilities/__init__.py +0 -0
  37. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib/utilities/sql.py +0 -0
  38. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib.egg-info/SOURCES.txt +0 -0
  39. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib.egg-info/dependency_links.txt +0 -0
  40. {moose_lib-0.6.110 → moose_lib-0.6.111}/moose_lib.egg-info/top_level.txt +0 -0
  41. {moose_lib-0.6.110 → moose_lib-0.6.111}/setup.cfg +0 -0
  42. {moose_lib-0.6.110 → moose_lib-0.6.111}/tests/__init__.py +0 -0
  43. {moose_lib-0.6.110 → moose_lib-0.6.111}/tests/conftest.py +0 -0
  44. {moose_lib-0.6.110 → moose_lib-0.6.111}/tests/test_moose.py +0 -0
  45. {moose_lib-0.6.110 → moose_lib-0.6.111}/tests/test_query_builder.py +0 -0
  46. {moose_lib-0.6.110 → moose_lib-0.6.111}/tests/test_redis_client.py +0 -0
  47. {moose_lib-0.6.110 → moose_lib-0.6.111}/tests/test_s3queue_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moose_lib
3
- Version: 0.6.110
3
+ Version: 0.6.111
4
4
  Home-page: https://www.fiveonefour.com/moose
5
5
  Author: Fiveonefour Labs Inc.
6
6
  Author-email: support@fiveonefour.com
@@ -15,6 +15,7 @@ Requires-Dist: humanfriendly>=10.0
15
15
  Requires-Dist: clickhouse_connect>=0.7.16
16
16
  Requires-Dist: requests>=2.32.3
17
17
  Requires-Dist: sqlglot[rs]>=27.16.3
18
+ Requires-Dist: confluent-kafka[json,schemaregistry]>=2.11.1
18
19
  Dynamic: author
19
20
  Dynamic: author-email
20
21
  Dynamic: description
@@ -32,6 +32,7 @@ class KafkaConfig:
32
32
  sasl_mechanism: Optional[str] = None
33
33
  security_protocol: Optional[str] = None
34
34
  namespace: Optional[str] = None
35
+ schema_registry_url: Optional[str] = None
35
36
 
36
37
 
37
38
  @dataclass
@@ -103,6 +104,7 @@ def read_project_config() -> ProjectConfig:
103
104
  sasl_mechanism=sec.get("sasl_mechanism"),
104
105
  security_protocol=sec.get("security_protocol"),
105
106
  namespace=sec.get("namespace"),
107
+ schema_registry_url=sec.get("schema_registry_url"),
106
108
  )
107
109
 
108
110
  kafka_cfg = _parse_kafka("kafka_config")
@@ -30,6 +30,7 @@ class RuntimeKafkaConfig:
30
30
  sasl_mechanism: Optional[str]
31
31
  security_protocol: Optional[str]
32
32
  namespace: Optional[str]
33
+ schema_registry_url: Optional[str]
33
34
 
34
35
 
35
36
  class ConfigurationRegistry:
@@ -160,6 +161,8 @@ class ConfigurationRegistry:
160
161
  _env("MOOSE_KAFKA_CONFIG__SECURITY_PROTOCOL")
161
162
  namespace = _env("MOOSE_REDPANDA_CONFIG__NAMESPACE") or \
162
163
  _env("MOOSE_KAFKA_CONFIG__NAMESPACE")
164
+ schema_registry_url = _env("MOOSE_REDPANDA_CONFIG__SCHEMA_REGISTRY_URL") or \
165
+ _env("MOOSE_KAFKA_CONFIG__SCHEMA_REGISTRY_URL")
163
166
 
164
167
  file_kafka = config.kafka_config
165
168
 
@@ -181,6 +184,8 @@ class ConfigurationRegistry:
181
184
  security_protocol=security_protocol if security_protocol is not None else (
182
185
  file_kafka.security_protocol if file_kafka else None),
183
186
  namespace=namespace if namespace is not None else (file_kafka.namespace if file_kafka else None),
187
+ schema_registry_url=schema_registry_url if schema_registry_url is not None else (
188
+ file_kafka.schema_registry_url if file_kafka else None),
184
189
  )
185
190
  except Exception as e:
186
191
  raise RuntimeError(f"Failed to get Kafka configuration: {e}")
@@ -28,6 +28,10 @@ from .stream import (
28
28
  Stream,
29
29
  DeadLetterModel,
30
30
  DeadLetterQueue,
31
+ SubjectLatest,
32
+ SubjectVersion,
33
+ SchemaById,
34
+ KafkaSchemaConfig,
31
35
  )
32
36
 
33
37
  from .ingest_api import (
@@ -117,6 +121,10 @@ __all__ = [
117
121
  'Stream',
118
122
  'DeadLetterModel',
119
123
  'DeadLetterQueue',
124
+ 'SubjectLatest',
125
+ 'SubjectVersion',
126
+ 'SchemaById',
127
+ 'KafkaSchemaConfig',
120
128
 
121
129
  # Ingestion
122
130
  'IngestConfig',
@@ -8,7 +8,7 @@ import dataclasses
8
8
  import datetime
9
9
  import json
10
10
  from typing import Any, Optional, Callable, Union, Literal, Generic
11
- from pydantic import BaseModel, ConfigDict, AliasGenerator
11
+ from pydantic import BaseModel, ConfigDict, AliasGenerator, Field
12
12
  from pydantic.alias_generators import to_camel
13
13
  from kafka import KafkaProducer
14
14
 
@@ -20,6 +20,24 @@ from ..config.runtime import config_registry, RuntimeKafkaConfig
20
20
  from ..commons import get_kafka_producer
21
21
 
22
22
 
23
+ class SubjectLatest(BaseModel):
24
+ name: str = Field(serialization_alias="subjectLatest")
25
+
26
+
27
+ class SubjectVersion(BaseModel):
28
+ subject: str
29
+ version: int
30
+
31
+
32
+ class SchemaById(BaseModel):
33
+ id: int
34
+
35
+
36
+ class KafkaSchemaConfig(BaseModel):
37
+ kind: Literal["JSON", "AVRO", "PROTOBUF"]
38
+ reference: Union[SubjectLatest, SubjectVersion, SchemaById]
39
+
40
+
23
41
  class StreamConfig(BaseModel):
24
42
  """Configuration for data streams (e.g., Redpanda topics).
25
43
 
@@ -41,6 +59,7 @@ class StreamConfig(BaseModel):
41
59
  default_dead_letter_queue: "Optional[DeadLetterQueue]" = None
42
60
  # allow DeadLetterQueue
43
61
  model_config = ConfigDict(arbitrary_types_allowed=True)
62
+ schema_config: Optional[KafkaSchemaConfig] = None
44
63
 
45
64
 
46
65
  class TransformConfig(BaseModel):
@@ -249,9 +268,9 @@ class Stream(TypedMooseResource, Generic[T]):
249
268
  return []
250
269
  return [b.strip() for b in broker_string.split(",") if b.strip()]
251
270
 
252
- def _get_memoized_producer(self) -> tuple[KafkaProducer, Any]:
271
+ def _get_memoized_producer(self) -> tuple[KafkaProducer, RuntimeKafkaConfig]:
253
272
  """Create or reuse a KafkaProducer using runtime configuration."""
254
- cfg = config_registry.get_kafka_config()
273
+ cfg: RuntimeKafkaConfig = config_registry.get_kafka_config()
255
274
  current_hash = self._create_kafka_config_hash(cfg)
256
275
 
257
276
  if self._memoized_producer is not None and self._kafka_config_hash == current_hash:
@@ -267,16 +286,16 @@ class Stream(TypedMooseResource, Generic[T]):
267
286
  finally:
268
287
  self._memoized_producer = None
269
288
 
270
- brokers = self._parse_brokers(getattr(cfg, "broker", "localhost:19092"))
289
+ brokers = self._parse_brokers(cfg.broker)
271
290
  if not brokers:
272
- raise RuntimeError(f"No valid broker addresses found in: '{getattr(cfg, 'broker', '')}'")
291
+ raise RuntimeError(f"No valid broker addresses found in: '{cfg.broker}'")
273
292
 
274
293
  producer = get_kafka_producer(
275
294
  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),
295
+ sasl_username=cfg.sasl_username,
296
+ sasl_password=cfg.sasl_password,
297
+ sasl_mechanism=cfg.sasl_mechanism,
298
+ security_protocol=cfg.security_protocol,
280
299
  value_serializer=lambda v: v.model_dump_json().encode('utf-8'),
281
300
  acks="all",
282
301
  )
@@ -300,7 +319,9 @@ class Stream(TypedMooseResource, Generic[T]):
300
319
  def send(self, values: ZeroOrMany[T]) -> None:
301
320
  """Send one or more records to this stream's Kafka topic.
302
321
 
303
- Values are JSON-serialized using EnhancedJSONEncoder.
322
+ If `schema_registry` (JSON) is configured, resolve schema id and
323
+ send using Confluent wire format (0x00 + 4-byte schema id + JSON bytes).
324
+ Otherwise, values are JSON-serialized.
304
325
  """
305
326
  # Normalize inputs to a flat list of records
306
327
  filtered: list[T] = []
@@ -326,11 +347,42 @@ class Stream(TypedMooseResource, Generic[T]):
326
347
  )
327
348
 
328
349
  producer, cfg = self._get_memoized_producer()
329
- topic = self._build_full_topic_name(getattr(cfg, "namespace", None))
350
+ topic = self._build_full_topic_name(cfg.namespace)
330
351
 
331
- for rec in filtered:
332
- producer.send(topic, value=rec)
333
- producer.flush()
352
+ sr = self.config.schema_config
353
+ if sr is not None:
354
+ if sr.kind != "JSON":
355
+ raise NotImplementedError("Currently JSON Schema is supported.")
356
+ try:
357
+ from confluent_kafka.schema_registry import SchemaRegistryClient
358
+ from confluent_kafka.schema_registry.json_schema import JSONSerializer
359
+ except Exception as e:
360
+ raise RuntimeError(
361
+ "confluent-kafka[json,schemaregistry] is required for Schema Registry JSON"
362
+ ) from e
363
+
364
+ sr_url = cfg.schema_registry_url
365
+ if not sr_url:
366
+ raise RuntimeError("Schema Registry URL not configured")
367
+ client = SchemaRegistryClient({"url": sr_url})
368
+
369
+ if isinstance(sr.reference, SchemaById):
370
+ schema = client.get_schema(sr.reference.id)
371
+ elif isinstance(sr.reference, SubjectLatest):
372
+ schema = client.get_latest_version(sr.reference.name).schema
373
+ else:
374
+ schema = client.get_version(sr.reference.subject, sr.reference.version).schema
375
+
376
+ serializer = JSONSerializer(schema, client)
377
+
378
+ for rec in filtered:
379
+ value_bytes = serializer(rec.model_dump())
380
+ producer.send(topic, value=value_bytes)
381
+ producer.flush()
382
+ else:
383
+ for rec in filtered:
384
+ producer.send(topic, value=rec)
385
+ producer.flush()
334
386
 
335
387
 
336
388
  class DeadLetterModel(BaseModel, Generic[T]):
@@ -24,6 +24,7 @@ from moose_lib.dmv2 import (
24
24
  MaterializedView,
25
25
  SqlResource
26
26
  )
27
+ from moose_lib.dmv2.stream import KafkaSchemaConfig
27
28
  from pydantic.alias_generators import to_camel
28
29
  from pydantic.json_schema import JsonSchemaValue
29
30
 
@@ -163,6 +164,7 @@ class TopicConfig(BaseModel):
163
164
  consumers: List[Consumer]
164
165
  metadata: Optional[dict] = None
165
166
  life_cycle: Optional[str] = None
167
+ schema_config: Optional[KafkaSchemaConfig] = None
166
168
 
167
169
 
168
170
  class IngestApiConfig(BaseModel):
@@ -477,6 +479,7 @@ def to_infra_map() -> dict:
477
479
  consumers=consumers,
478
480
  metadata=getattr(stream, "metadata", None),
479
481
  life_cycle=stream.config.life_cycle.value if stream.config.life_cycle else None,
482
+ schema_config=stream.config.schema_config,
480
483
  )
481
484
 
482
485
  for name, api in get_ingest_apis().items():
@@ -299,11 +299,19 @@ def create_consumer() -> KafkaConsumer:
299
299
  Returns:
300
300
  Configured KafkaConsumer instance
301
301
  """
302
+ def _sr_json_deserializer(m: bytes):
303
+ if m is None:
304
+ return None
305
+ # Schema Registry JSON envelope: 0x00 + 4-byte schema ID (big-endian) + JSON
306
+ if len(m) >= 5 and m[0] == 0x00:
307
+ m = m[5:]
308
+ return json.loads(m.decode("utf-8"))
309
+
302
310
  kwargs = dict(
303
311
  broker=broker,
304
312
  client_id="python_streaming_function_consumer",
305
313
  group_id=streaming_function_id,
306
- value_deserializer=lambda m: json.loads(m.decode("utf-8")),
314
+ value_deserializer=_sr_json_deserializer,
307
315
  sasl_username=sasl_config.get("username"),
308
316
  sasl_password=sasl_config.get("password"),
309
317
  sasl_mechanism=sasl_config.get("mechanism"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: moose_lib
3
- Version: 0.6.110
3
+ Version: 0.6.111
4
4
  Home-page: https://www.fiveonefour.com/moose
5
5
  Author: Fiveonefour Labs Inc.
6
6
  Author-email: support@fiveonefour.com
@@ -15,6 +15,7 @@ Requires-Dist: humanfriendly>=10.0
15
15
  Requires-Dist: clickhouse_connect>=0.7.16
16
16
  Requires-Dist: requests>=2.32.3
17
17
  Requires-Dist: sqlglot[rs]>=27.16.3
18
+ Requires-Dist: confluent-kafka[json,schemaregistry]>=2.11.1
18
19
  Dynamic: author
19
20
  Dynamic: author-email
20
21
  Dynamic: description
@@ -7,3 +7,4 @@ humanfriendly>=10.0
7
7
  clickhouse_connect>=0.7.16
8
8
  requests>=2.32.3
9
9
  sqlglot[rs]>=27.16.3
10
+ confluent-kafka[json,schemaregistry]>=2.11.1
@@ -33,5 +33,6 @@ setup(
33
33
  "clickhouse_connect>=0.7.16",
34
34
  "requests>=2.32.3",
35
35
  "sqlglot[rs]>=27.16.3",
36
+ "confluent-kafka[json,schemaregistry]>=2.11.1"
36
37
  ],
37
38
  )
File without changes
File without changes