API refactor
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-07 16:25:52 +09:00
parent 76d0d86211
commit 91c7e04474
1171 changed files with 81940 additions and 44117 deletions

View File

@@ -1,4 +1,9 @@
from .base import BaseParser, _AsyncRESPBase
from .base import (
AsyncPushNotificationsParser,
BaseParser,
PushNotificationsParser,
_AsyncRESPBase,
)
from .commands import AsyncCommandsParser, CommandsParser
from .encoders import Encoder
from .hiredis import _AsyncHiredisParser, _HiredisParser
@@ -11,10 +16,12 @@ __all__ = [
"_AsyncRESPBase",
"_AsyncRESP2Parser",
"_AsyncRESP3Parser",
"AsyncPushNotificationsParser",
"CommandsParser",
"Encoder",
"BaseParser",
"_HiredisParser",
"_RESP2Parser",
"_RESP3Parser",
"PushNotificationsParser",
]

View File

@@ -1,7 +1,7 @@
import sys
from abc import ABC
from asyncio import IncompleteReadError, StreamReader, TimeoutError
from typing import List, Optional, Union
from typing import Callable, List, Optional, Protocol, Union
if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
from asyncio import timeout as async_timeout
@@ -9,26 +9,32 @@ else:
from async_timeout import timeout as async_timeout
from ..exceptions import (
AskError,
AuthenticationError,
AuthenticationWrongNumberOfArgsError,
BusyLoadingError,
ClusterCrossSlotError,
ClusterDownError,
ConnectionError,
ExecAbortError,
MasterDownError,
ModuleError,
MovedError,
NoPermissionError,
NoScriptError,
OutOfMemoryError,
ReadOnlyError,
RedisError,
ResponseError,
TryAgainError,
)
from ..typing import EncodableT
from .encoders import Encoder
from .socket import SERVER_CLOSED_CONNECTION_ERROR, SocketBuffer
MODULE_LOAD_ERROR = "Error loading the extension. " "Please check the server logs."
MODULE_LOAD_ERROR = "Error loading the extension. Please check the server logs."
NO_SUCH_MODULE_ERROR = "Error unloading module: no such module with that name"
MODULE_UNLOAD_NOT_POSSIBLE_ERROR = "Error unloading module: operation not " "possible."
MODULE_UNLOAD_NOT_POSSIBLE_ERROR = "Error unloading module: operation not possible."
MODULE_EXPORTS_DATA_TYPES_ERROR = (
"Error unloading module: the module "
"exports one or more module-side data "
@@ -72,6 +78,12 @@ class BaseParser(ABC):
"READONLY": ReadOnlyError,
"NOAUTH": AuthenticationError,
"NOPERM": NoPermissionError,
"ASK": AskError,
"TRYAGAIN": TryAgainError,
"MOVED": MovedError,
"CLUSTERDOWN": ClusterDownError,
"CROSSSLOT": ClusterCrossSlotError,
"MASTERDOWN": MasterDownError,
}
@classmethod
@@ -146,6 +158,58 @@ class AsyncBaseParser(BaseParser):
raise NotImplementedError()
_INVALIDATION_MESSAGE = [b"invalidate", "invalidate"]
class PushNotificationsParser(Protocol):
"""Protocol defining RESP3-specific parsing functionality"""
pubsub_push_handler_func: Callable
invalidation_push_handler_func: Optional[Callable] = None
def handle_pubsub_push_response(self, response):
"""Handle pubsub push responses"""
raise NotImplementedError()
def handle_push_response(self, response, **kwargs):
if response[0] not in _INVALIDATION_MESSAGE:
return self.pubsub_push_handler_func(response)
if self.invalidation_push_handler_func:
return self.invalidation_push_handler_func(response)
def set_pubsub_push_handler(self, pubsub_push_handler_func):
self.pubsub_push_handler_func = pubsub_push_handler_func
def set_invalidation_push_handler(self, invalidation_push_handler_func):
self.invalidation_push_handler_func = invalidation_push_handler_func
class AsyncPushNotificationsParser(Protocol):
"""Protocol defining async RESP3-specific parsing functionality"""
pubsub_push_handler_func: Callable
invalidation_push_handler_func: Optional[Callable] = None
async def handle_pubsub_push_response(self, response):
"""Handle pubsub push responses asynchronously"""
raise NotImplementedError()
async def handle_push_response(self, response, **kwargs):
"""Handle push responses asynchronously"""
if response[0] not in _INVALIDATION_MESSAGE:
return await self.pubsub_push_handler_func(response)
if self.invalidation_push_handler_func:
return await self.invalidation_push_handler_func(response)
def set_pubsub_push_handler(self, pubsub_push_handler_func):
"""Set the pubsub push handler function"""
self.pubsub_push_handler_func = pubsub_push_handler_func
def set_invalidation_push_handler(self, invalidation_push_handler_func):
"""Set the invalidation push handler function"""
self.invalidation_push_handler_func = invalidation_push_handler_func
class _AsyncRESPBase(AsyncBaseParser):
"""Base class for async resp parsing"""
@@ -182,7 +246,7 @@ class _AsyncRESPBase(AsyncBaseParser):
return True
try:
async with async_timeout(0):
return await self._stream.read(1)
return self._stream.at_eof()
except TimeoutError:
return False

View File

@@ -38,7 +38,7 @@ def parse_info(response):
response = str_if_bytes(response)
def get_value(value):
if "," not in value or "=" not in value:
if "," not in value and "=" not in value:
try:
if "." in value:
return float(value)
@@ -46,11 +46,18 @@ def parse_info(response):
return int(value)
except ValueError:
return value
elif "=" not in value:
return [get_value(v) for v in value.split(",") if v]
else:
sub_dict = {}
for item in value.split(","):
k, v = item.rsplit("=", 1)
sub_dict[k] = get_value(v)
if not item:
continue
if "=" in item:
k, v = item.rsplit("=", 1)
sub_dict[k] = get_value(v)
else:
sub_dict[item] = True
return sub_dict
for line in response.splitlines():
@@ -80,7 +87,7 @@ def parse_memory_stats(response, **kwargs):
"""Parse the results of MEMORY STATS"""
stats = pairs_to_dict(response, decode_keys=True, decode_string_values=True)
for key, value in stats.items():
if key.startswith("db."):
if key.startswith("db.") and isinstance(value, list):
stats[key] = pairs_to_dict(
value, decode_keys=True, decode_string_values=True
)
@@ -268,17 +275,22 @@ def parse_xinfo_stream(response, **options):
data = {str_if_bytes(k): v for k, v in response.items()}
if not options.get("full", False):
first = data.get("first-entry")
if first is not None:
if first is not None and first[0] is not None:
data["first-entry"] = (first[0], pairs_to_dict(first[1]))
last = data["last-entry"]
if last is not None:
if last is not None and last[0] is not None:
data["last-entry"] = (last[0], pairs_to_dict(last[1]))
else:
data["entries"] = {_id: pairs_to_dict(entry) for _id, entry in data["entries"]}
if isinstance(data["groups"][0], list):
if len(data["groups"]) > 0 and isinstance(data["groups"][0], list):
data["groups"] = [
pairs_to_dict(group, decode_keys=True) for group in data["groups"]
]
for g in data["groups"]:
if g["consumers"] and g["consumers"][0] is not None:
g["consumers"] = [
pairs_to_dict(c, decode_keys=True) for c in g["consumers"]
]
else:
data["groups"] = [
{str_if_bytes(k): v for k, v in group.items()}
@@ -322,7 +334,7 @@ def float_or_none(response):
return float(response)
def bool_ok(response):
def bool_ok(response, **options):
return str_if_bytes(response) == "OK"
@@ -354,7 +366,12 @@ def parse_scan(response, **options):
def parse_hscan(response, **options):
cursor, r = response
return int(cursor), r and pairs_to_dict(r) or {}
no_values = options.get("no_values", False)
if no_values:
payload = r or []
else:
payload = r and pairs_to_dict(r) or {}
return int(cursor), payload
def parse_zscan(response, **options):
@@ -379,13 +396,20 @@ def parse_slowlog_get(response, **options):
# an O(N) complexity) instead of the command.
if isinstance(item[3], list):
result["command"] = space.join(item[3])
result["client_address"] = item[4]
result["client_name"] = item[5]
# These fields are optional, depends on environment.
if len(item) >= 6:
result["client_address"] = item[4]
result["client_name"] = item[5]
else:
result["complexity"] = item[3]
result["command"] = space.join(item[4])
result["client_address"] = item[5]
result["client_name"] = item[6]
# These fields are optional, depends on environment.
if len(item) >= 7:
result["client_address"] = item[5]
result["client_name"] = item[6]
return result
return [parse_item(item) for item in response]
@@ -428,9 +452,11 @@ def parse_cluster_info(response, **options):
def _parse_node_line(line):
line_items = line.split(" ")
node_id, addr, flags, master_id, ping, pong, epoch, connected = line.split(" ")[:8]
addr = addr.split("@")[0]
ip = addr.split("@")[0]
hostname = addr.split("@")[1].split(",")[1] if "@" in addr and "," in addr else ""
node_dict = {
"node_id": node_id,
"hostname": hostname,
"flags": flags,
"master_id": master_id,
"last_ping_sent": ping,
@@ -443,7 +469,7 @@ def _parse_node_line(line):
if len(line_items) >= 9:
slots, migrations = _parse_slots(line_items[8:])
node_dict["slots"], node_dict["migrations"] = slots, migrations
return addr, node_dict
return ip, node_dict
def _parse_slots(slot_ranges):
@@ -490,7 +516,7 @@ def parse_geosearch_generic(response, **options):
except KeyError: # it means the command was sent via execute_command
return response
if type(response) != list:
if not isinstance(response, list):
response_list = [response]
else:
response_list = response
@@ -650,7 +676,8 @@ def parse_client_info(value):
"omem",
"tot-mem",
}:
client_info[int_key] = int(client_info[int_key])
if int_key in client_info:
client_info[int_key] = int(client_info[int_key])
return client_info
@@ -813,24 +840,28 @@ _RedisCallbacksRESP2 = {
_RedisCallbacksRESP3 = {
**string_keys_to_dict(
"SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
),
**string_keys_to_dict(
"ZRANGE ZINTER ZPOPMAX ZPOPMIN ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE "
"ZUNION HGETALL XREADGROUP",
lambda r, **kwargs: r,
),
**string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3),
"ACL LOG": lambda r: [
{str_if_bytes(key): str_if_bytes(value) for key, value in x.items()} for x in r
]
if isinstance(r, list)
else bool_ok(r),
"ACL LOG": lambda r: (
[
{str_if_bytes(key): str_if_bytes(value) for key, value in x.items()}
for x in r
]
if isinstance(r, list)
else bool_ok(r)
),
"COMMAND": parse_command_resp3,
"CONFIG GET": lambda r: {
str_if_bytes(key)
if key is not None
else None: str_if_bytes(value)
if value is not None
else None
str_if_bytes(key) if key is not None else None: (
str_if_bytes(value) if value is not None else None
)
for key, value in r.items()
},
"MEMORY STATS": lambda r: {str_if_bytes(key): value for key, value in r.items()},
@@ -838,11 +869,11 @@ _RedisCallbacksRESP3 = {
"SENTINEL MASTERS": parse_sentinel_masters_resp3,
"SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_resp3,
"SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_resp3,
"STRALGO": lambda r, **options: {
str_if_bytes(key): str_if_bytes(value) for key, value in r.items()
}
if isinstance(r, dict)
else str_if_bytes(r),
"STRALGO": lambda r, **options: (
{str_if_bytes(key): str_if_bytes(value) for key, value in r.items()}
if isinstance(r, dict)
else str_if_bytes(r)
),
"XINFO CONSUMERS": lambda r: [
{str_if_bytes(key): value for key, value in x.items()} for x in r
],

View File

@@ -1,19 +1,23 @@
import asyncio
import socket
import sys
from typing import Callable, List, Optional, Union
from logging import getLogger
from typing import Callable, List, Optional, TypedDict, Union
if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
from asyncio import timeout as async_timeout
else:
from async_timeout import timeout as async_timeout
from redis.compat import TypedDict
from ..exceptions import ConnectionError, InvalidResponse, RedisError
from ..typing import EncodableT
from ..utils import HIREDIS_AVAILABLE
from .base import AsyncBaseParser, BaseParser
from .base import (
AsyncBaseParser,
AsyncPushNotificationsParser,
BaseParser,
PushNotificationsParser,
)
from .socket import (
NONBLOCKING_EXCEPTION_ERROR_NUMBERS,
NONBLOCKING_EXCEPTIONS,
@@ -21,6 +25,11 @@ from .socket import (
SERVER_CLOSED_CONNECTION_ERROR,
)
# Used to signal that hiredis-py does not have enough data to parse.
# Using `False` or `None` is not reliable, given that the parser can
# return `False` or `None` for legitimate reasons from RESP payloads.
NOT_ENOUGH_DATA = object()
class _HiredisReaderArgs(TypedDict, total=False):
protocolError: Callable[[str], Exception]
@@ -29,7 +38,7 @@ class _HiredisReaderArgs(TypedDict, total=False):
errors: Optional[str]
class _HiredisParser(BaseParser):
class _HiredisParser(BaseParser, PushNotificationsParser):
"Parser class for connections using Hiredis"
def __init__(self, socket_read_size):
@@ -37,6 +46,9 @@ class _HiredisParser(BaseParser):
raise RedisError("Hiredis is not installed")
self.socket_read_size = socket_read_size
self._buffer = bytearray(socket_read_size)
self.pubsub_push_handler_func = self.handle_pubsub_push_response
self.invalidation_push_handler_func = None
self._hiredis_PushNotificationType = None
def __del__(self):
try:
@@ -44,6 +56,11 @@ class _HiredisParser(BaseParser):
except Exception:
pass
def handle_pubsub_push_response(self, response):
logger = getLogger("push_response")
logger.debug("Push response: " + str(response))
return response
def on_connect(self, connection, **kwargs):
import hiredis
@@ -53,25 +70,32 @@ class _HiredisParser(BaseParser):
"protocolError": InvalidResponse,
"replyError": self.parse_error,
"errors": connection.encoder.encoding_errors,
"notEnoughData": NOT_ENOUGH_DATA,
}
if connection.encoder.decode_responses:
kwargs["encoding"] = connection.encoder.encoding
self._reader = hiredis.Reader(**kwargs)
self._next_response = False
self._next_response = NOT_ENOUGH_DATA
try:
self._hiredis_PushNotificationType = hiredis.PushNotification
except AttributeError:
# hiredis < 3.2
self._hiredis_PushNotificationType = None
def on_disconnect(self):
self._sock = None
self._reader = None
self._next_response = False
self._next_response = NOT_ENOUGH_DATA
def can_read(self, timeout):
if not self._reader:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
if self._next_response is False:
if self._next_response is NOT_ENOUGH_DATA:
self._next_response = self._reader.gets()
if self._next_response is False:
if self._next_response is NOT_ENOUGH_DATA:
return self.read_from_socket(timeout=timeout, raise_on_timeout=False)
return True
@@ -105,14 +129,24 @@ class _HiredisParser(BaseParser):
if custom_timeout:
sock.settimeout(self._socket_timeout)
def read_response(self, disable_decoding=False):
def read_response(self, disable_decoding=False, push_request=False):
if not self._reader:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
# _next_response might be cached from a can_read() call
if self._next_response is not False:
if self._next_response is not NOT_ENOUGH_DATA:
response = self._next_response
self._next_response = False
self._next_response = NOT_ENOUGH_DATA
if self._hiredis_PushNotificationType is not None and isinstance(
response, self._hiredis_PushNotificationType
):
response = self.handle_push_response(response)
if not push_request:
return self.read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:
return response
return response
if disable_decoding:
@@ -120,7 +154,7 @@ class _HiredisParser(BaseParser):
else:
response = self._reader.gets()
while response is False:
while response is NOT_ENOUGH_DATA:
self.read_from_socket()
if disable_decoding:
response = self._reader.gets(False)
@@ -131,6 +165,16 @@ class _HiredisParser(BaseParser):
# happened
if isinstance(response, ConnectionError):
raise response
elif self._hiredis_PushNotificationType is not None and isinstance(
response, self._hiredis_PushNotificationType
):
response = self.handle_push_response(response)
if not push_request:
return self.read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:
return response
elif (
isinstance(response, list)
and response
@@ -140,7 +184,7 @@ class _HiredisParser(BaseParser):
return response
class _AsyncHiredisParser(AsyncBaseParser):
class _AsyncHiredisParser(AsyncBaseParser, AsyncPushNotificationsParser):
"""Async implementation of parser class for connections using Hiredis"""
__slots__ = ("_reader",)
@@ -150,6 +194,14 @@ class _AsyncHiredisParser(AsyncBaseParser):
raise RedisError("Hiredis is not available.")
super().__init__(socket_read_size=socket_read_size)
self._reader = None
self.pubsub_push_handler_func = self.handle_pubsub_push_response
self.invalidation_push_handler_func = None
self._hiredis_PushNotificationType = None
async def handle_pubsub_push_response(self, response):
logger = getLogger("push_response")
logger.debug("Push response: " + str(response))
return response
def on_connect(self, connection):
import hiredis
@@ -158,6 +210,7 @@ class _AsyncHiredisParser(AsyncBaseParser):
kwargs: _HiredisReaderArgs = {
"protocolError": InvalidResponse,
"replyError": self.parse_error,
"notEnoughData": NOT_ENOUGH_DATA,
}
if connection.encoder.decode_responses:
kwargs["encoding"] = connection.encoder.encoding
@@ -166,13 +219,21 @@ class _AsyncHiredisParser(AsyncBaseParser):
self._reader = hiredis.Reader(**kwargs)
self._connected = True
try:
self._hiredis_PushNotificationType = getattr(
hiredis, "PushNotification", None
)
except AttributeError:
# hiredis < 3.2
self._hiredis_PushNotificationType = None
def on_disconnect(self):
self._connected = False
async def can_read_destructive(self):
if not self._connected:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
if self._reader.gets():
if self._reader.gets() is not NOT_ENOUGH_DATA:
return True
try:
async with async_timeout(0):
@@ -190,7 +251,7 @@ class _AsyncHiredisParser(AsyncBaseParser):
return True
async def read_response(
self, disable_decoding: bool = False
self, disable_decoding: bool = False, push_request: bool = False
) -> Union[EncodableT, List[EncodableT]]:
# If `on_disconnect()` has been called, prohibit any more reads
# even if they could happen because data might be present.
@@ -198,16 +259,33 @@ class _AsyncHiredisParser(AsyncBaseParser):
if not self._connected:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR) from None
response = self._reader.gets()
while response is False:
await self.read_from_socket()
if disable_decoding:
response = self._reader.gets(False)
else:
response = self._reader.gets()
while response is NOT_ENOUGH_DATA:
await self.read_from_socket()
if disable_decoding:
response = self._reader.gets(False)
else:
response = self._reader.gets()
# if the response is a ConnectionError or the response is a list and
# the first item is a ConnectionError, raise it as something bad
# happened
if isinstance(response, ConnectionError):
raise response
elif self._hiredis_PushNotificationType is not None and isinstance(
response, self._hiredis_PushNotificationType
):
response = await self.handle_push_response(response)
if not push_request:
return await self.read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:
return response
elif (
isinstance(response, list)
and response

View File

@@ -3,20 +3,26 @@ from typing import Any, Union
from ..exceptions import ConnectionError, InvalidResponse, ResponseError
from ..typing import EncodableT
from .base import _AsyncRESPBase, _RESPBase
from .base import (
AsyncPushNotificationsParser,
PushNotificationsParser,
_AsyncRESPBase,
_RESPBase,
)
from .socket import SERVER_CLOSED_CONNECTION_ERROR
class _RESP3Parser(_RESPBase):
class _RESP3Parser(_RESPBase, PushNotificationsParser):
"""RESP3 protocol implementation"""
def __init__(self, socket_read_size):
super().__init__(socket_read_size)
self.push_handler_func = self.handle_push_response
self.pubsub_push_handler_func = self.handle_pubsub_push_response
self.invalidation_push_handler_func = None
def handle_push_response(self, response):
def handle_pubsub_push_response(self, response):
logger = getLogger("push_response")
logger.info("Push response: " + str(response))
logger.debug("Push response: " + str(response))
return response
def read_response(self, disable_decoding=False, push_request=False):
@@ -85,19 +91,16 @@ class _RESP3Parser(_RESPBase):
# set response
elif byte == b"~":
# redis can return unhashable types (like dict) in a set,
# so we need to first convert to a list, and then try to convert it to a set
# so we return sets as list, all the time, for predictability
response = [
self._read_response(disable_decoding=disable_decoding)
for _ in range(int(response))
]
try:
response = set(response)
except TypeError:
pass
# map response
elif byte == b"%":
# we use this approach and not dict comprehension here
# because this dict comprehension fails in python 3.7
# We cannot use a dict-comprehension to parse stream.
# Evaluation order of key:val expression in dict comprehension only
# became defined to be left-right in version 3.8
resp_dict = {}
for _ in range(int(response)):
key = self._read_response(disable_decoding=disable_decoding)
@@ -113,13 +116,13 @@ class _RESP3Parser(_RESPBase):
)
for _ in range(int(response))
]
res = self.push_handler_func(response)
response = self.handle_push_response(response)
if not push_request:
return self._read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:
return res
return response
else:
raise InvalidResponse(f"Protocol Error: {raw!r}")
@@ -127,18 +130,16 @@ class _RESP3Parser(_RESPBase):
response = self.encoder.decode(response)
return response
def set_push_handler(self, push_handler_func):
self.push_handler_func = push_handler_func
class _AsyncRESP3Parser(_AsyncRESPBase):
class _AsyncRESP3Parser(_AsyncRESPBase, AsyncPushNotificationsParser):
def __init__(self, socket_read_size):
super().__init__(socket_read_size)
self.push_handler_func = self.handle_push_response
self.pubsub_push_handler_func = self.handle_pubsub_push_response
self.invalidation_push_handler_func = None
def handle_push_response(self, response):
async def handle_pubsub_push_response(self, response):
logger = getLogger("push_response")
logger.info("Push response: " + str(response))
logger.debug("Push response: " + str(response))
return response
async def read_response(
@@ -214,23 +215,23 @@ class _AsyncRESP3Parser(_AsyncRESPBase):
# set response
elif byte == b"~":
# redis can return unhashable types (like dict) in a set,
# so we need to first convert to a list, and then try to convert it to a set
# so we always convert to a list, to have predictable return types
response = [
(await self._read_response(disable_decoding=disable_decoding))
for _ in range(int(response))
]
try:
response = set(response)
except TypeError:
pass
# map response
elif byte == b"%":
response = {
(await self._read_response(disable_decoding=disable_decoding)): (
await self._read_response(disable_decoding=disable_decoding)
# We cannot use a dict-comprehension to parse stream.
# Evaluation order of key:val expression in dict comprehension only
# became defined to be left-right in version 3.8
resp_dict = {}
for _ in range(int(response)):
key = await self._read_response(disable_decoding=disable_decoding)
resp_dict[key] = await self._read_response(
disable_decoding=disable_decoding, push_request=push_request
)
for _ in range(int(response))
}
response = resp_dict
# push response
elif byte == b">":
response = [
@@ -241,19 +242,16 @@ class _AsyncRESP3Parser(_AsyncRESPBase):
)
for _ in range(int(response))
]
res = self.push_handler_func(response)
response = await self.handle_push_response(response)
if not push_request:
return await self._read_response(
disable_decoding=disable_decoding, push_request=push_request
)
else:
return res
return response
else:
raise InvalidResponse(f"Protocol Error: {raw!r}")
if isinstance(response, bytes) and disable_decoding is False:
response = self.encoder.decode(response)
return response
def set_push_handler(self, push_handler_func):
self.push_handler_func = push_handler_func