Squashed 'utils_v2/' content from commit d593c1ec
git-subtree-dir: utils_v2 git-subtree-split: d593c1ec43b43c6881c40cbef6ae7c170dc76776
This commit is contained in:
@@ -0,0 +1,523 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Saturday, 13th Jul, 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To be able to work with keys
|
||||
|
||||
REFERENCES:
|
||||
|
||||
N/A
|
||||
|
||||
DOWNLOADS:
|
||||
|
||||
N/A
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** IMPORT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# To make sibling directories accessible for imports:
|
||||
import sys
|
||||
sys.path.append(".")
|
||||
sys.path.append("..")
|
||||
|
||||
# To use Kafka:
|
||||
from aiokafka import AIOKafkaProducer
|
||||
from aiokafka import AIOKafkaConsumer
|
||||
|
||||
# For working with JSON strings:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.serialization.json_serializer import JSONSerializer
|
||||
|
||||
# Data models:
|
||||
from utils_v2.queue.kafka.models.message import ConsumedKafkaMessage
|
||||
|
||||
# For debugging:
|
||||
from icecream import IceCreamDebugger
|
||||
|
||||
# For SSL security:
|
||||
import ssl
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
def get_ssl_context(
|
||||
ca_file,
|
||||
cert_file,
|
||||
key_file
|
||||
):
|
||||
|
||||
"""
|
||||
Generate the SSL context to use with the Kafka instances.
|
||||
:param ca_file: The Certificate Authority file as a path to a local file.
|
||||
:param cert_file: The Certificate file as a path to a local file.
|
||||
:param key_file: The Key file as a path to a local file.
|
||||
:return: The SSL context instance as a path to a local file.
|
||||
"""
|
||||
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.load_verify_locations(ca_file)
|
||||
ssl_context.load_cert_chain(certfile = cert_file, keyfile = key_file)
|
||||
return ssl_context
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** CLASSES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
class ProducerKafka:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
topic,
|
||||
serializer = None,
|
||||
debug = True,
|
||||
debug_prefix = "Kafka (P) | ",
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Create a Kafka Producer.
|
||||
:param topic: The topic to produce on.
|
||||
:param serializer: The serializer to use.
|
||||
:param debug: Whether, or not, you want to print the debug strings.
|
||||
:param debug_prefix: The prefix to use while debugging.
|
||||
:param kwargs: Any configuration parameters for the Kafka instances.
|
||||
"""
|
||||
|
||||
# Initialize the debugger:
|
||||
self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
|
||||
if not debug: self.__printer.disable()
|
||||
|
||||
# initialize the Kafka producer:
|
||||
self.__topic = topic
|
||||
self.__kwargs = kwargs
|
||||
self.__producer = None
|
||||
self.__connected = False
|
||||
self.__serializer = serializer or JSONSerializer()
|
||||
|
||||
# For establishing connection:
|
||||
self.__exclusive_semaphore = asyncio.Semaphore(1)
|
||||
|
||||
def enable_debug(self):
|
||||
self.__printer.enable()
|
||||
|
||||
def disable_debug(self):
|
||||
self.__printer.disable()
|
||||
|
||||
async def connect(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
async with self.__exclusive_semaphore:
|
||||
if not self.__connected:
|
||||
try:
|
||||
self.__producer = AIOKafkaProducer(**self.__kwargs)
|
||||
await self.__producer.start()
|
||||
self.__connected = True
|
||||
except Exception as exception: self.__printer(exception)
|
||||
return self.__connected
|
||||
|
||||
async def ensure_connection(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
if not self.__connected: await self.connect()
|
||||
return self.__connected
|
||||
|
||||
async def close(self):
|
||||
|
||||
"""
|
||||
Terminates the connection.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
if self.__connected:
|
||||
try:
|
||||
await self.__producer.stop()
|
||||
self.__printer("Producer closed!")
|
||||
self.__connected = False
|
||||
except Exception as exception: self.__printer(exception)
|
||||
|
||||
async def produce(self, value, key = None, topic = None, encoding = "utf-8"):
|
||||
|
||||
"""
|
||||
Sends one message to the Kafka server on the topic that has been set for this instance.
|
||||
:param value: The message to send.
|
||||
:param key: The key to use when you want the messages to follow an order.
|
||||
:param topic: A custom topic for this message, else the topic defined during the creation of this instance will
|
||||
be used by default.
|
||||
:param encoding: The encoding format.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
# Ensure connectivity to the server.
|
||||
# If not connected, return with failure immediately.
|
||||
if not await self.ensure_connection(): return False
|
||||
|
||||
try:
|
||||
|
||||
# Send the message:
|
||||
await self.__producer.send_and_wait(
|
||||
topic = topic or self.__topic,
|
||||
value = self.__serializer.serialize(data = value, encoding = encoding),
|
||||
key = key
|
||||
)
|
||||
|
||||
# Return with success if no exception occurred:
|
||||
return True
|
||||
|
||||
# Return with failure if something went wrong:
|
||||
except Exception as exception:
|
||||
self.__printer(exception, self.__topic, type(value), value)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ConsumerKafka:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
topic,
|
||||
serializer = None,
|
||||
debug = True,
|
||||
debug_prefix = "Kafka (C) | ",
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Create a Kafka Consumer.
|
||||
:param topic: The topic to consumer on.
|
||||
:param serializer: The serializer to use.
|
||||
:param debug: Whether, or not, you want to print the debug strings.
|
||||
:param debug_prefix: The prefix to use while debugging.
|
||||
:param kwargs: Any configuration parameters for the Kafka instances.
|
||||
"""
|
||||
|
||||
# Initialize the debugger:
|
||||
self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
|
||||
if not debug: self.__printer.disable()
|
||||
|
||||
# initialize the Kafka producer:
|
||||
self.__topic = topic
|
||||
self.__kwargs = kwargs
|
||||
self.__consumer = None
|
||||
self.__connected = False
|
||||
self.__serializer = serializer or JSONSerializer()
|
||||
|
||||
# For establishing connection:
|
||||
self.__exclusive_semaphore = asyncio.Semaphore(1)
|
||||
|
||||
def enable_debug(self):
|
||||
self.__printer.enable()
|
||||
|
||||
def disable_debug(self):
|
||||
self.__printer.disable()
|
||||
|
||||
async def connect(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
async with self.__exclusive_semaphore:
|
||||
if not self.__connected:
|
||||
try:
|
||||
self.__consumer = AIOKafkaConsumer(self.__topic, **self.__kwargs)
|
||||
await self.__consumer.start()
|
||||
self.__connected = True
|
||||
except Exception as exception: self.__printer(exception)
|
||||
return self.__connected
|
||||
|
||||
async def ensure_connection(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
if not self.__connected: await self.connect()
|
||||
return self.__connected
|
||||
|
||||
async def close(self):
|
||||
|
||||
"""
|
||||
Terminates the connection.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
if self.__connected:
|
||||
try:
|
||||
await self.__consumer.stop()
|
||||
self.__printer("Consumer closed!")
|
||||
self.__connected = False
|
||||
except Exception as exception: self.__printer(exception)
|
||||
|
||||
async def consume(self, count = 1, timeout = 0.05, encoding = "utf-8"):
|
||||
|
||||
"""
|
||||
Get messages from the Kafka server.
|
||||
:param count: The number of messages to get from the Kafka server.
|
||||
:param timeout: The time in seconds to wait for retrieval.
|
||||
:param encoding: The encoding to use.
|
||||
:return: The messages that were received. If no messages are available, an empty list will be returned.
|
||||
"""
|
||||
|
||||
# Ensure connectivity to the server.
|
||||
# If not connected, return with failure immediately.
|
||||
if not await self.ensure_connection(): return []
|
||||
|
||||
# Make a variable that will hold the final results:
|
||||
messages = []
|
||||
|
||||
try:
|
||||
|
||||
# Read some messages:
|
||||
results = await self.__consumer.getmany(
|
||||
max_records = max(1, count),
|
||||
timeout_ms = int(timeout * 1_000)
|
||||
)
|
||||
|
||||
# Format the received messages:
|
||||
if results:
|
||||
for topic_partition, records in results.items():
|
||||
for record in records:
|
||||
messages.append(ConsumedKafkaMessage.from_aiokafka(
|
||||
message = record,
|
||||
deserializer = lambda x: self.__serializer.deserialize(x, encoding = encoding)
|
||||
))
|
||||
|
||||
# Debugging print if something went wrong:
|
||||
except Exception as exception: self.__printer(exception)
|
||||
|
||||
# Done here:
|
||||
return messages
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BidirectionalKafka:
|
||||
|
||||
# The 'roles' that the instance can take.
|
||||
# The master talks on the channel (topic) that the slave listens on and vice versa.
|
||||
# Master-Slave is only for deciding who talks on which channel and who listens on which.
|
||||
# In a two-party system, one must be the master, the other must be the slave.
|
||||
# There are no extra privileges that the master enjoys. The naming convention was borrowed from common protocols
|
||||
# used in electronics (like I2C).
|
||||
ROLE_MASTER = 1
|
||||
ROLE_SLAVE = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
role,
|
||||
topic,
|
||||
ack_topic: str = None,
|
||||
group: str = None,
|
||||
serializer = None,
|
||||
debug = True,
|
||||
debug_prefix = "Kafka (B) | ",
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Creates a walkie-talkie type setup to use Kafka in a bidirectional manner. Fo more information on all the
|
||||
individual methods, please read through the doc-strings of the component classes 'ProducerKafka', and
|
||||
'ConsumerKafka'.
|
||||
:param role: Select from "ROLE_MASTER" and "ROLE_SLAVE". Between the two parties that are talking, one will be
|
||||
the master and the other will be the slave. The channel that the master uses to speak will the one the slave
|
||||
uses to listen, and vice versa.
|
||||
:param topic: The topic to communicate on. Will be the same between the master and the slave.
|
||||
:param ack_topic: Explicitly provide this for the second channel, or it will be created from the name of the
|
||||
topic itself. Will be the same between the master and the slave.
|
||||
:param group: The group to assign the instance to.
|
||||
:param debug: Whether, or not, you want to print the debug strings.
|
||||
:param debug_prefix: The prefix to use while debugging.
|
||||
:param kwargs: Any configuration parameters for the Kafka instances.
|
||||
"""
|
||||
|
||||
# Not down the basic variables:
|
||||
self.__role = role
|
||||
self.__topic = topic
|
||||
self.__ack_topic = ack_topic or topic + "Ack"
|
||||
self.__group = group
|
||||
|
||||
# In case the current instance is the master,
|
||||
# it will talk on "topic", and listen on "ack_topic":
|
||||
if self.__role == self.ROLE_MASTER:
|
||||
self.__producer_kwargs = kwargs.copy()
|
||||
self.__producer = ProducerKafka(
|
||||
topic = self.__topic,
|
||||
serializer = serializer,
|
||||
debug = debug,
|
||||
debug_prefix = debug_prefix.strip() + " (P) | ",
|
||||
**self.__producer_kwargs
|
||||
)
|
||||
self.__consumer_kwargs = kwargs.copy()
|
||||
self.__consumer_kwargs["group_id"] = self.__group
|
||||
self.__consumer = ConsumerKafka(
|
||||
topic = self.__ack_topic,
|
||||
group = group,
|
||||
serializer = serializer,
|
||||
debug = debug,
|
||||
debug_prefix = debug_prefix.strip() + " (C) | ",
|
||||
**self.__consumer_kwargs
|
||||
)
|
||||
|
||||
# On the other hand, if the current instance is a slave,
|
||||
# It will listen on "topic", and talk on "ack_topic":
|
||||
else:
|
||||
self.__producer_kwargs = kwargs.copy()
|
||||
self.__producer = ProducerKafka(
|
||||
topic = self.__ack_topic,
|
||||
serializer = serializer,
|
||||
debug = debug,
|
||||
debug_prefix = debug_prefix.strip() + " (P) | ",
|
||||
**self.__producer_kwargs
|
||||
)
|
||||
self.__consumer_kwargs = kwargs.copy()
|
||||
self.__consumer_kwargs["group_id"] = self.__group
|
||||
self.__consumer = ConsumerKafka(
|
||||
topic = self.__topic,
|
||||
group = group,
|
||||
serializer = serializer,
|
||||
debug = debug,
|
||||
debug_prefix = debug_prefix.strip() + " (C) | ",
|
||||
**self.__consumer_kwargs
|
||||
)
|
||||
|
||||
def enable_debug(self):
|
||||
self.__producer.enable_debug()
|
||||
self.__consumer.enable_debug()
|
||||
|
||||
def disable_debug(self):
|
||||
self.__producer.disable_debug()
|
||||
self.__consumer.disable_debug()
|
||||
|
||||
async def ensure_connection(self):
|
||||
await self.__producer.ensure_connection()
|
||||
await self.__consumer.ensure_connection()
|
||||
|
||||
async def close(self):
|
||||
await self.__producer.close()
|
||||
await self.__consumer.close()
|
||||
|
||||
async def produce(self, message, encoding = "utf-8"):
|
||||
return await self.__producer.produce(message, encoding = encoding)
|
||||
|
||||
async def consume(self, count = 1, timeout = 0.05, encoding = "utf-8"):
|
||||
return await self.__consumer.consume(count = count, timeout = timeout, encoding = encoding)
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
# from data_models.kafka_message import KafkaMessage
|
||||
|
||||
ssl_ctx = get_ssl_context(
|
||||
ca_file = r"/home/developer/PycharmProjects/utils/cred/kafka/cert_authority.pem",
|
||||
cert_file = r"/home/developer/PycharmProjects/utils/cred/kafka/fullchain.pem",
|
||||
key_file = r"/home/developer/PycharmProjects/utils/cred/kafka/privkey.pem"
|
||||
)
|
||||
|
||||
async def consumer_test():
|
||||
|
||||
consumer = ConsumerKafka(
|
||||
topic = "tick-listners",
|
||||
# group_id = "assessImg",
|
||||
group_id = "updateMedia",
|
||||
bootstrap_servers = "wtt.ditscentre.in:9092",
|
||||
security_protocol = "SSL",
|
||||
ssl_context = ssl_ctx
|
||||
)
|
||||
await consumer.connect()
|
||||
await asyncio.sleep(1.5)
|
||||
print("READY!")
|
||||
|
||||
while True:
|
||||
messages = await consumer.consume(count = 1)
|
||||
for message in messages: print(message)
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
async def producer_test():
|
||||
|
||||
producer = ProducerKafka(
|
||||
topic = "tick-listners",
|
||||
bootstrap_servers = "del.ditscentre.in:9092",
|
||||
security_protocol = "SSL",
|
||||
ssl_context = ssl_ctx
|
||||
)
|
||||
await producer.connect()
|
||||
print("READY!")
|
||||
|
||||
while True:
|
||||
success = await producer.produce({"name": "Bhopli", "color": "orange"})
|
||||
print("PRODUCED:", success)
|
||||
await asyncio.sleep(0.25)
|
||||
|
||||
await producer.close()
|
||||
|
||||
|
||||
asyncio.run(consumer_test())
|
||||
@@ -0,0 +1,557 @@
|
||||
"""
|
||||
|
||||
AUTHOR:
|
||||
|
||||
Khushal P Soonderji
|
||||
|
||||
DATE:
|
||||
|
||||
Friday, 20th Dec., 2024
|
||||
|
||||
OBJECTIVE:
|
||||
|
||||
To work with Kafka in a synchronous way. This is a translation of the wrapper originally made for asynchronous
|
||||
operation.
|
||||
|
||||
REFERENCES:
|
||||
|
||||
01. https://docs.confluent.io/platform/current/clients/confluent-kafka-python/html/index.html#
|
||||
|
||||
DOWNLOADS:
|
||||
|
||||
N/A
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** IMPORT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# To make sibling directories accessible for imports:
|
||||
import sys
|
||||
sys.path.append(".")
|
||||
sys.path.append("..")
|
||||
|
||||
# To use Kafka:
|
||||
from confluent_kafka import Producer
|
||||
from confluent_kafka import Consumer
|
||||
|
||||
# For working with JSON strings:
|
||||
from utils_v2.string import json
|
||||
from utils_v2.serialization.json_serializer import JSONSerializer
|
||||
|
||||
# Data models:
|
||||
from utils_v2.queue.kafka.models.message import ConsumedKafkaMessage
|
||||
|
||||
# For debugging:
|
||||
from icecream import IceCreamDebugger
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import List, Literal
|
||||
|
||||
# To work with date and time:
|
||||
import time
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MACROS / ONE-TIME INIT ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** VARIABLES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** FUNCTIONS ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
# --- Nothing Yet
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** CLASSES ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
class ProducerKafka:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: dict,
|
||||
topic: str,
|
||||
serializer = None,
|
||||
debug: bool = True,
|
||||
debug_prefix = "Kafka (P) | "
|
||||
):
|
||||
|
||||
"""
|
||||
Create a Kafka Producer.
|
||||
:param config: The configuration as expected by Confluent-Kafka library.
|
||||
:param topic: The topic to produce on.
|
||||
:param serializer: The serializer to use.
|
||||
:param debug: Whether, or not, you want to print the debug strings.
|
||||
:param debug_prefix: The prefix to use while debugging.
|
||||
"""
|
||||
|
||||
# Initialize the debugger:
|
||||
self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
|
||||
if not debug: self.__printer.disable()
|
||||
|
||||
# initialize the Kafka producer:
|
||||
self.__topic = topic
|
||||
self.__config = config
|
||||
self.__producer = None
|
||||
self.__connected = False
|
||||
self.__serializer = serializer or JSONSerializer()
|
||||
|
||||
def enable_debug(self):
|
||||
self.__printer.enable()
|
||||
|
||||
def disable_debug(self):
|
||||
self.__printer.disable()
|
||||
|
||||
@staticmethod
|
||||
def create_config(
|
||||
bootstrap_servers: str | List[str],
|
||||
security_protocol: Literal["PLAINTEXT", "SSL"] = "PLAINTEXT",
|
||||
ca_file: str | None = None,
|
||||
cert_file: str | None = None,
|
||||
key_file: str | None = None,
|
||||
client_id: str | int | None = None
|
||||
):
|
||||
|
||||
"""
|
||||
Creates the config required for Confluent-Kafka's library.
|
||||
:param bootstrap_servers: The addresses of the Kafka brokers.
|
||||
:param security_protocol: What sort of security protocol to use.
|
||||
:param ca_file: Needed for 'SSL' security protocol.
|
||||
:param cert_file: Needed for 'SSL' security protocol.
|
||||
:param key_file: Needed for 'SSL' security protocol.
|
||||
:param client_id: An identifier for one producer. Useful for debugging later.
|
||||
:return: The dictionary that needs to be passed as the 'conf' param when creating the producer.
|
||||
"""
|
||||
|
||||
# Start with the bare minimum:
|
||||
if not isinstance(bootstrap_servers, list): bootstrap_servers = [bootstrap_servers]
|
||||
config = {
|
||||
"bootstrap.servers": ",".join(bootstrap_servers),
|
||||
"security.protocol": security_protocol
|
||||
}
|
||||
|
||||
# Add the SSL security details:
|
||||
if security_protocol == "SSL":
|
||||
config["ssl.ca.location"] = ca_file
|
||||
config["ssl.certificate.location"] = cert_file
|
||||
config["ssl.key.location"] = key_file
|
||||
|
||||
# Add an identifier for debugging:
|
||||
if client_id: config["client.id"] = client_id
|
||||
|
||||
# Done here:
|
||||
return config
|
||||
|
||||
@property
|
||||
def client(self) -> Producer:
|
||||
return self.__producer
|
||||
|
||||
def connect(self) -> bool:
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
if not self.__connected:
|
||||
try:
|
||||
self.__producer = Producer(self.__config)
|
||||
self.__connected = True
|
||||
except Exception as exception: self.__printer(exception)
|
||||
return self.__connected
|
||||
|
||||
def ensure_connection(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
if not self.__connected: self.connect()
|
||||
return self.__connected
|
||||
|
||||
def flush(self):
|
||||
|
||||
"""
|
||||
Flushes the buffer entirely.
|
||||
:return: None
|
||||
"""
|
||||
|
||||
start_time = time.time()
|
||||
self.__producer.flush()
|
||||
message = f"Producer flushed in {time.time() - start_time:.5f} second(s)."
|
||||
self.__printer(message)
|
||||
|
||||
def close(self):
|
||||
|
||||
"""
|
||||
Terminates the connection.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
if self.__connected:
|
||||
try:
|
||||
self.flush()
|
||||
self.__printer("Producer closed!")
|
||||
self.__connected = False
|
||||
except Exception as exception: self.__printer(exception)
|
||||
return not self.__connected
|
||||
|
||||
def produce(
|
||||
self,
|
||||
value,
|
||||
key = None,
|
||||
topic = None,
|
||||
encoding = "utf-8",
|
||||
callback = None
|
||||
) -> bool:
|
||||
|
||||
"""
|
||||
Sends one message to the Kafka server on the topic that has been set for this instance.
|
||||
:param value: The message to send.
|
||||
:param key: The key to use when you want the messages to follow an order.
|
||||
:param topic: A custom topic for this message, else the topic defined during the creation of this instance will
|
||||
be used by default.
|
||||
:param encoding: The encoding format.
|
||||
:param callback: The function to call for delivery reports.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
# Ensure connectivity to the server.
|
||||
# If not connected, return with failure immediately.
|
||||
if not self.ensure_connection(): return False
|
||||
|
||||
try:
|
||||
|
||||
# Send the message:
|
||||
self.__producer.produce(
|
||||
topic = topic or self.__topic,
|
||||
value = self.__serializer.serialize(data = value, encoding = encoding),
|
||||
key = key,
|
||||
callback = callback
|
||||
)
|
||||
|
||||
# Return with success if no exception occurred:
|
||||
return True
|
||||
|
||||
# Return with failure if something went wrong:
|
||||
except Exception as exception:
|
||||
self.__printer(exception, self.__topic, type(value), value)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ConsumerKafka:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: dict,
|
||||
topic: str,
|
||||
serializer = None,
|
||||
debug = True,
|
||||
debug_prefix = "Kafka (C) | ",
|
||||
**kwargs
|
||||
):
|
||||
|
||||
"""
|
||||
Create a Kafka Consumer.
|
||||
:param topic: the topic to consume on.
|
||||
:param serializer: The serializer to use.
|
||||
:param debug: Whether, or not, you want to print the debug strings.
|
||||
:param debug_prefix: The prefix to use while debugging.
|
||||
:param kwargs: Any configuration parameters for the Kafka instances.
|
||||
"""
|
||||
|
||||
# Initialize the debugger:
|
||||
self.__printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
|
||||
if not debug: self.__printer.disable()
|
||||
|
||||
# initialize the Kafka consumer:
|
||||
self.__config = config
|
||||
self.__topic = topic
|
||||
self.__kwargs = kwargs
|
||||
self.__consumer = None
|
||||
self.__connected = False
|
||||
self.__serializer = serializer or JSONSerializer()
|
||||
|
||||
def enable_debug(self):
|
||||
self.__printer.enable()
|
||||
|
||||
def disable_debug(self):
|
||||
self.__printer.disable()
|
||||
|
||||
@staticmethod
|
||||
def create_config(
|
||||
bootstrap_servers: str | List[str],
|
||||
group_id: str = "default",
|
||||
auto_offset_reset: Literal["latest", "earliest"] = "latest",
|
||||
security_protocol: Literal["PLAINTEXT", "SSL"] = "PLAINTEXT",
|
||||
ca_file: str | None = None,
|
||||
cert_file: str | None = None,
|
||||
key_file: str | None = None,
|
||||
client_id: str | int | None = None
|
||||
):
|
||||
|
||||
"""
|
||||
Creates the config required for Confluent-Kafka's library.
|
||||
:param bootstrap_servers: The addresses of the Kafka brokers.
|
||||
:param group_id: When a set of consumers are working on one topic in a group such that you want only one of them
|
||||
to read a particular message.
|
||||
:param auto_offset_reset: Use this to influence the behaviour of how the Kafka consumer will read messages when
|
||||
it first connects to the broker. It could either want to read the earliest (oldest) messages or the latest
|
||||
(newest) messages from the queue.
|
||||
:param security_protocol: What sort of security protocol to use.
|
||||
:param ca_file: Needed for 'SSL' security protocol.
|
||||
:param cert_file: Needed for 'SSL' security protocol.
|
||||
:param key_file: Needed for 'SSL' security protocol.
|
||||
:param client_id: An identifier for one producer. Useful for debugging later.
|
||||
:return: The dictionary that needs to be passed as the 'conf' param when creating the producer.
|
||||
"""
|
||||
|
||||
# Start with the bare minimum:
|
||||
if not isinstance(bootstrap_servers, list): bootstrap_servers = [bootstrap_servers]
|
||||
config = {
|
||||
"bootstrap.servers": ",".join(bootstrap_servers),
|
||||
"security.protocol": security_protocol
|
||||
}
|
||||
|
||||
# Add the SSL security details:
|
||||
if security_protocol == "SSL":
|
||||
config["ssl.ca.location"] = ca_file
|
||||
config["ssl.certificate.location"] = cert_file
|
||||
config["ssl.key.location"] = key_file
|
||||
|
||||
# Add an identifier for debugging:
|
||||
if client_id: config["client.id"] = client_id
|
||||
|
||||
# Consumer-specific:
|
||||
config["group.id"] = group_id
|
||||
config["auto.offset.reset"] = auto_offset_reset
|
||||
|
||||
# Done here:
|
||||
return config
|
||||
|
||||
@property
|
||||
def client(self) -> Consumer:
|
||||
return self.__consumer
|
||||
|
||||
def connect(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
if not self.__connected:
|
||||
try:
|
||||
if not isinstance(self.__topic, list): self.__topic = [self.__topic]
|
||||
self.__consumer = Consumer(self.__config)
|
||||
self.__consumer.subscribe(self.__topic)
|
||||
self.__connected = True
|
||||
except Exception as exception: self.__printer(exception)
|
||||
return self.__connected
|
||||
|
||||
def ensure_connection(self):
|
||||
|
||||
"""
|
||||
Connects to the Kafka server if not connected.
|
||||
:return: True or False based on the success of the operation.
|
||||
"""
|
||||
|
||||
if not self.__connected: self.connect()
|
||||
return self.__connected
|
||||
|
||||
def close(self):
|
||||
|
||||
"""
|
||||
Terminates the connection.
|
||||
:return: None.
|
||||
"""
|
||||
|
||||
if self.__connected:
|
||||
try:
|
||||
self.__consumer.close()
|
||||
self.__printer("Consumer closed!")
|
||||
self.__connected = False
|
||||
except Exception as exception: self.__printer(exception)
|
||||
|
||||
def __consume_one(
|
||||
self,
|
||||
timeout = 0.05,
|
||||
encoding = "utf-8"
|
||||
) -> dict | None:
|
||||
|
||||
"""
|
||||
Here we consume exactly one message from the broker. If we need multiple messages (a batch of messages), we call
|
||||
this method as many times as needed.
|
||||
:param timeout: The time in seconds to wait for retrieval.
|
||||
:param encoding: The encoding to use.
|
||||
:return: A dict that holds the details of the message. Null if no message was fetched.
|
||||
"""
|
||||
|
||||
message = self.__consumer.poll(timeout = timeout)
|
||||
if message is None or message.error(): return None
|
||||
else: return ConsumedKafkaMessage.from_confluent_kafka(
|
||||
message = message,
|
||||
deserializer = lambda x: self.__serializer.deserialize(x, encoding = encoding)
|
||||
)
|
||||
# message_value = message.value()
|
||||
# if message_value is not None: message_value = self.__serializer.deserialize(message_value, encoding = encoding)
|
||||
# return {
|
||||
# "topic": message.topic(),
|
||||
# "partition": message.partition(),
|
||||
# "offset": message.offset(),
|
||||
# "key": message.key().decode("utf-8") if message.key() else None,
|
||||
# "value": message_value,
|
||||
# "timestamp": message.timestamp()[1],
|
||||
# "headers": message.headers()
|
||||
# }
|
||||
|
||||
def consume(self, count = 1, timeout = 0.05, encoding = "utf-8"):
|
||||
|
||||
"""
|
||||
Get messages from the Kafka broker.
|
||||
:param count: The number of messages to get from the Kafka server.
|
||||
:param timeout: The time in seconds to wait for retrieval.
|
||||
:param encoding: The encoding to use.
|
||||
:return: The messages that were received. If no messages are available, an empty list will be returned.
|
||||
"""
|
||||
|
||||
# Make some variables:
|
||||
start_time = time.time()
|
||||
messages = []
|
||||
|
||||
# Ensure connectivity to the server.
|
||||
# If not connected, return with failure immediately.
|
||||
if not self.ensure_connection(): return messages
|
||||
|
||||
# Get into an indefinite loop.
|
||||
while True:
|
||||
|
||||
# If timed-out, break out of the loop:
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time > timeout: break
|
||||
|
||||
# Get the next message:
|
||||
message = self.__consume_one(
|
||||
timeout = timeout - elapsed_time,
|
||||
encoding = encoding
|
||||
)
|
||||
|
||||
# If a message was fetched in the timeout, append it to the list of messages.
|
||||
# Break out of the loop if you have reached the needed no. of messages:
|
||||
if message:
|
||||
messages.append(message)
|
||||
if len(messages) >= count: break
|
||||
|
||||
# Done here:
|
||||
return messages
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** MAIN PROGRAM ***
|
||||
# ***** ****
|
||||
# *****************************************************************************************************************
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
def producer_test(
|
||||
count: int = 10,
|
||||
interval: float = 1.0
|
||||
):
|
||||
|
||||
# Create and connect the producer:
|
||||
producer = ProducerKafka(
|
||||
topic = "kft_file_upload",
|
||||
config = ProducerKafka.create_config(
|
||||
bootstrap_servers = "del.ditscentre.in:9092",
|
||||
security_protocol = "SSL",
|
||||
ca_file = r"../../creds/kafka/cert_authority.pem",
|
||||
cert_file = r"../../creds/kafka/fullchain.pem",
|
||||
key_file = r"../../creds/kafka/privkey.pem",
|
||||
client_id = 123
|
||||
)
|
||||
)
|
||||
producer.connect()
|
||||
print("PRODUCER READY!")
|
||||
|
||||
# Send the message a number of times:
|
||||
for message_no in range(count):
|
||||
my_message = {
|
||||
"ts": datetime.datetime.now().timestamp(),
|
||||
"msgNo": message_no,
|
||||
"payload": {
|
||||
"name": "Bhopli",
|
||||
"color": "orange",
|
||||
"age": "just a baby",
|
||||
}
|
||||
}
|
||||
success = producer.produce(my_message)
|
||||
print("PRODUCED:", success)
|
||||
time.sleep(interval)
|
||||
|
||||
# Ensure a graceful close:
|
||||
producer.close()
|
||||
|
||||
def consumer_test():
|
||||
|
||||
# Create and connect the producer:
|
||||
consumer = ConsumerKafka(
|
||||
topic = "tick-listners",
|
||||
config = ConsumerKafka.create_config(
|
||||
bootstrap_servers = "del.ditscentre.in:9092",
|
||||
group_id = "test-group",
|
||||
security_protocol = "SSL",
|
||||
ca_file = r"../../../../creds/kafka/cert_authority.pem",
|
||||
cert_file = r"../../../../creds/kafka/fullchain.pem",
|
||||
key_file = r"../../../../creds/kafka/privkey.pem",
|
||||
)
|
||||
)
|
||||
consumer.connect()
|
||||
print("CONSUMER READY!")
|
||||
|
||||
for _ in range(500):
|
||||
messages = consumer.consume(count = 3, timeout = 2.5)
|
||||
print(json.to_string(messages, default = str))
|
||||
|
||||
consumer.close()
|
||||
|
||||
# Run the test code:
|
||||
# producer_test(count = 5)
|
||||
consumer_test()
|
||||
Reference in New Issue
Block a user