diff --git a/redis_clone/parser/__init__.py b/redis_clone/parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/redis_clone/parser/base.py b/redis_clone/parser/base.py new file mode 100644 index 0000000..74834a1 --- /dev/null +++ b/redis_clone/parser/base.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod +from typing import Tuple, List, AnyStr + +class BaseParser(ABC): + @abstractmethod + def parse(self, data, *args, **kwargs) -> Tuple[AnyStr, List[AnyStr]]: + pass \ No newline at end of file diff --git a/redis_clone/parser/hi_redis_parser.py b/redis_clone/parser/hi_redis_parser.py new file mode 100644 index 0000000..30e1f3e --- /dev/null +++ b/redis_clone/parser/hi_redis_parser.py @@ -0,0 +1,18 @@ +from hiredis import Reader + +from redis_clone.parser.base import BaseParser + + +class HiRedisParser(BaseParser): + def __init__(self): + self.reader_class = Reader + + def parse(self, data, *args, **kwargs): + reader = self.reader_class(*args, **kwargs) + reader.feed(data) + if data := reader.gets(): + command = data[0] + arguments = data[1:] + return command, arguments + + raise Exception("Invalid data received") \ No newline at end of file diff --git a/redis_clone/redis_parser.py b/redis_clone/parser/redis_parser.py similarity index 98% rename from redis_clone/redis_parser.py rename to redis_clone/parser/redis_parser.py index f1e2da9..6d8dee8 100644 --- a/redis_clone/redis_parser.py +++ b/redis_clone/parser/redis_parser.py @@ -1,5 +1,7 @@ from enum import Enum +from redis_clone.parser.base import BaseParser + PROTOCOL_SEPARATOR = b'\r\n' COMMANDS_METADATA = { @@ -31,11 +33,11 @@ class Protocol_2_Data_Types(Enum): ARRAY = b"*" -class Parser: +class Parser(BaseParser): def __init__(self, protocol_version) -> None: self.protocol_version = protocol_version - def parse_client_request(self, data): + def parse(self, data, *args, **kwargs): """ This function parses the client request and returns the command name and arguments """ diff --git a/redis_clone/response_builder.py b/redis_clone/response_builder.py index 85f2d86..3943f0f 100644 --- a/redis_clone/response_builder.py +++ b/redis_clone/response_builder.py @@ -1,4 +1,4 @@ -from redis_clone.redis_parser import Protocol_2_Data_Types, PROTOCOL_SEPARATOR +from redis_clone.parser.redis_parser import Protocol_2_Data_Types, PROTOCOL_SEPARATOR class ResponseBuilder: diff --git a/redis_clone/server.py b/redis_clone/server.py index d86a146..00a7740 100644 --- a/redis_clone/server.py +++ b/redis_clone/server.py @@ -1,18 +1,19 @@ import time -import sys import os import asyncio import logging from enum import Enum -from redis_clone.redis_parser import Parser, Protocol_2_Data_Types +from hiredis import Reader +from redis_clone.parser.redis_parser import Parser, Protocol_2_Data_Types +from redis_clone.parser.hi_redis_parser import HiRedisParser from redis_clone.response_builder import ResponseBuilder logger = logging.getLogger(__name__) HOST = os.environ.get("REDIS_HOST", "0.0.0.0") -PORT = os.environ.get("REDIS_PORT", 9999) +PORT = int(os.environ.get("REDIS_PORT", 9999)) class Protocol_2_Commands(Enum): @@ -94,7 +95,7 @@ async def _handle_connection(self, reader, writer): break logger.info(f"Received data: {data}") - command_name, command_args = self.parser.parse_client_request(data) + command_name, command_args = self.parser.parse(data, encoding="utf-8", errors="strict") logger.info(f"Command name: {command_name}") logger.info(f"Command args: {command_args}") response = self._process_command(command_name, command_args) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8cf7f7c..57a035a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ async-timeout==4.0.3 exceptiongroup==1.1.3 +hiredis==2.3.2 iniconfig==2.0.0 packaging==23.2 pluggy==1.3.0 diff --git a/tests/test_client_parser.py b/tests/test_client_parser.py index 75d09f6..8fdd8e4 100644 --- a/tests/test_client_parser.py +++ b/tests/test_client_parser.py @@ -1,5 +1,5 @@ # Using pytest for tests -from redis_clone.redis_parser import Parser, Protocol_2_Data_Types +from redis_clone.parser.redis_parser import Parser, Protocol_2_Data_Types class TestParserClient: @@ -11,7 +11,7 @@ def test_initial_command_request(self): # Test initial connection test_str = b"*1\r\n$7\r\nCOMMAND\r\n" - command, args = self.parser.parse_client_request(test_str) + command, args = self.parser.parse(test_str) assert command == "COMMAND" assert args == [] @@ -21,7 +21,7 @@ def test_set_command_request(self): Test SET command request """ test_str = b"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n" - command, args = self.parser.parse_client_request(test_str) + command, args = self.parser.parse(test_str) assert command == "SET" assert args == ["mykey", "myvalue"] @@ -31,7 +31,7 @@ def test_get_command_request(self): Test GET command request """ test_str = b"*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n" - command, args = self.parser.parse_client_request(test_str) + command, args = self.parser.parse(test_str) assert command == "GET" assert args == ["mykey"] @@ -42,7 +42,7 @@ def test_subargs_parsing(self): eg: SET mykey myvalue EX 10 NX ''' test_str = b"*6\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n$2\r\nEX\r\n$2\r\n10\r\n$2\r\nNX\r\n" - command, args = self.parser.parse_client_request(test_str) + command, args = self.parser.parse(test_str) print(args) assert command == "SET"