callite 0.1.4__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.
callite-0.1.4/PKG-INFO ADDED
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.1
2
+ Name: callite
3
+ Version: 0.1.4
4
+ Summary: Slim Redis RPC implementation
5
+ License: MIT
6
+ Author: Emrah Gozcu
7
+ Author-email: gozcu@gri.ai
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Requires-Dist: redis (>=5.0.3,<6.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # RPClite
16
+
17
+ RPClite is a lightweight Remote Procedure Call (RPC) implementation over Redis, designed to facilitate communication between different components of a distributed system. It minimizes dependencies and offers a simple yet effective solution for decoupling complex systems, thus alleviating potential library conflicts.
18
+
19
+ ## Setting up RPClite
20
+
21
+ Before using RPClite, ensure you have a Redis instance running. You can start a Redis server using the default settings or configure it as per your requirements.
22
+
23
+ ## Implementing the Server
24
+
25
+ To implement the RPClite server, follow these steps:
26
+
27
+ 1. Import the `RPCService` class from `server.rpc_server`.
28
+ 2. Define your main class and initialize the RPC service with the Redis URL and service name.
29
+ 3. Register your functions with the RPC service using the `register` decorator.
30
+ 4. Run the RPC service indefinitely.
31
+
32
+ Here's an example implementation:
33
+
34
+ ```python
35
+ from callite.server import RPCService
36
+
37
+
38
+ class Main:
39
+ def __init__(self):
40
+ self.service = "service"
41
+ self.redis_url = "redis://redis:6379/0"
42
+ self.rpc_service = RPCService(self.redis_url, self.service)
43
+
44
+ def run(self):
45
+ @self.rpc_service.register
46
+ def healthcheck():
47
+ return "OK"
48
+
49
+ self.rpc_service.run_forever()
50
+
51
+
52
+ if __name__ == "__main__":
53
+ Main().run()
54
+ ```
55
+
56
+ ## Calling the Function from Client
57
+
58
+ Once the server is set up, you can call functions remotely from the client side. Follow these steps to call functions:
59
+
60
+ 1. Import the `RPCClient` class from `client.rpc_client`.
61
+ 2. Define your client class and initialize the RPC client with the Redis URL and service name.
62
+ 3. Call the function using the `execute` method of the RPC client.
63
+ 4. Optionally, you can pass arguments and keyword arguments to the function.
64
+
65
+ Here's an example client implementation:
66
+
67
+ ```python
68
+ import time
69
+ from callite.client.rpc_client import RPCClient
70
+
71
+
72
+ class Healthcheck():
73
+ def __init__(self):
74
+ self.status = "OK"
75
+ self.r = RPCClient("redis://redis:6379/0", "service")
76
+
77
+ def get_status(self):
78
+ start = time.perf_counter()
79
+ self.status = self.r.execute('healthcheck')
80
+ end = time.perf_counter()
81
+ print(f"Healthcheck took {end - start:0.4f} seconds")
82
+ return self.status
83
+
84
+ def check(self):
85
+ return self.get_status()
86
+
87
+
88
+ if __name__ == "__main__":
89
+ Healthcheck().check()
90
+ ```
91
+
92
+ You can pass arguments and keyword arguments to the `execute` method as follows:
93
+
94
+ ```python
95
+ self.status = self.r.execute('healthcheck', [True], {'a': 1, 'b': 2})
96
+ ```
97
+
98
+ This setup allows for efficient communication between components of a distributed system, promoting modularity and scalability.
99
+ ```
@@ -0,0 +1,85 @@
1
+ # RPClite
2
+
3
+ RPClite is a lightweight Remote Procedure Call (RPC) implementation over Redis, designed to facilitate communication between different components of a distributed system. It minimizes dependencies and offers a simple yet effective solution for decoupling complex systems, thus alleviating potential library conflicts.
4
+
5
+ ## Setting up RPClite
6
+
7
+ Before using RPClite, ensure you have a Redis instance running. You can start a Redis server using the default settings or configure it as per your requirements.
8
+
9
+ ## Implementing the Server
10
+
11
+ To implement the RPClite server, follow these steps:
12
+
13
+ 1. Import the `RPCService` class from `server.rpc_server`.
14
+ 2. Define your main class and initialize the RPC service with the Redis URL and service name.
15
+ 3. Register your functions with the RPC service using the `register` decorator.
16
+ 4. Run the RPC service indefinitely.
17
+
18
+ Here's an example implementation:
19
+
20
+ ```python
21
+ from callite.server import RPCService
22
+
23
+
24
+ class Main:
25
+ def __init__(self):
26
+ self.service = "service"
27
+ self.redis_url = "redis://redis:6379/0"
28
+ self.rpc_service = RPCService(self.redis_url, self.service)
29
+
30
+ def run(self):
31
+ @self.rpc_service.register
32
+ def healthcheck():
33
+ return "OK"
34
+
35
+ self.rpc_service.run_forever()
36
+
37
+
38
+ if __name__ == "__main__":
39
+ Main().run()
40
+ ```
41
+
42
+ ## Calling the Function from Client
43
+
44
+ Once the server is set up, you can call functions remotely from the client side. Follow these steps to call functions:
45
+
46
+ 1. Import the `RPCClient` class from `client.rpc_client`.
47
+ 2. Define your client class and initialize the RPC client with the Redis URL and service name.
48
+ 3. Call the function using the `execute` method of the RPC client.
49
+ 4. Optionally, you can pass arguments and keyword arguments to the function.
50
+
51
+ Here's an example client implementation:
52
+
53
+ ```python
54
+ import time
55
+ from callite.client.rpc_client import RPCClient
56
+
57
+
58
+ class Healthcheck():
59
+ def __init__(self):
60
+ self.status = "OK"
61
+ self.r = RPCClient("redis://redis:6379/0", "service")
62
+
63
+ def get_status(self):
64
+ start = time.perf_counter()
65
+ self.status = self.r.execute('healthcheck')
66
+ end = time.perf_counter()
67
+ print(f"Healthcheck took {end - start:0.4f} seconds")
68
+ return self.status
69
+
70
+ def check(self):
71
+ return self.get_status()
72
+
73
+
74
+ if __name__ == "__main__":
75
+ Healthcheck().check()
76
+ ```
77
+
78
+ You can pass arguments and keyword arguments to the `execute` method as follows:
79
+
80
+ ```python
81
+ self.status = self.r.execute('healthcheck', [True], {'a': 1, 'b': 2})
82
+ ```
83
+
84
+ This setup allows for efficient communication between components of a distributed system, promoting modularity and scalability.
85
+ ```
@@ -0,0 +1,5 @@
1
+ # import all the classe
2
+ from .client import RPCClient
3
+ from .server import RPCServer
4
+ from .rpctypes import MessageBase, Request, Response, RPCException
5
+ from .shared import RedisConnection
@@ -0,0 +1,2 @@
1
+ from .rpc_client import *
2
+ __all__ = ['RPCClient']
@@ -0,0 +1,64 @@
1
+ import json
2
+ import threading
3
+ import logging
4
+
5
+ from callite.shared.redis_connection import RedisConnection
6
+ from callite.rpctypes.request import Request
7
+
8
+
9
+ # import pydevd_pycharm
10
+ # pydevd_pycharm.settrace('host.docker.internal', port=4444, stdoutToServer=True, stderrToServer=True)
11
+
12
+
13
+ class RPCClient(RedisConnection):
14
+
15
+ def __init__(self, conn_url: str, service: str, *args, **kwargs) -> None:
16
+ super().__init__(conn_url, service, *args, **kwargs)
17
+ self._request_pool = {}
18
+ self._subscribe()
19
+ self._logger = logging.getLogger(__name__)
20
+ self._logger.addHandler(logging.StreamHandler())
21
+ self._logger.setLevel(logging.INFO)
22
+
23
+ def _subscribe(self):
24
+ self.channel = self._rds.pubsub()
25
+ self.channel.subscribe(f'{self._queue_prefix}/response/{self._connection_id}')
26
+ thread = threading.Thread(target=self._pull_from_redis, daemon=True)
27
+ thread.start()
28
+
29
+ def _pull_from_redis(self):
30
+ def _on_delivery(message):
31
+ self._logger.info(f"Received message {message}")
32
+ data = json.loads(message['data'].decode('utf-8'))
33
+ request_guid = data['request_id']
34
+ if request_guid not in self._request_pool:
35
+ return
36
+ lock, _ = self._request_pool.pop(request_guid)
37
+ self._request_pool[request_guid] = (lock, data)
38
+ lock.release()
39
+
40
+ # TODO: handle poisonous messages from redis (e.g. non-json, old messages, etc.)
41
+ while self._running:
42
+ message: dict | None = self.channel.get_message(ignore_subscribe_messages=True, timeout=100)
43
+ if not message: continue
44
+ if not message['data']: continue
45
+ _on_delivery(message)
46
+
47
+
48
+ def execute(self, method: str, *args, **kwargs) -> dict:
49
+ request = Request(method, self._connection_id, None, *args, **kwargs)
50
+ request_uuid = request.request_id
51
+
52
+ request_lock = threading.Lock()
53
+ self._request_pool[request_uuid] = (request_lock, None)
54
+ request_lock.acquire()
55
+
56
+ self._rds.xadd(f'{self._queue_prefix}/request/{self._service}', {'data': json.dumps(request.payload_json())})
57
+ self._logger.info(f"Sent message {request_uuid} to {self._queue_prefix}/request/{self._service}")
58
+ # TODO: parameterize timeout
59
+ lock_success = request_lock.acquire(timeout=30)
60
+ lock, response = self._request_pool.pop(request_uuid)
61
+ if lock_success:
62
+ self._logger.info(f"Received response to message {request_uuid} {type(response['data'])}")
63
+ return response['data']
64
+ raise Exception('Timeout')
@@ -0,0 +1,6 @@
1
+ from .message_base import *
2
+ from .request import *
3
+ from .response import *
4
+ from .rpc_exception import *
5
+
6
+ __all__ = ['MessageBase', 'Request', 'Response', 'RPCException']
@@ -0,0 +1,7 @@
1
+ class MessageBase(object):
2
+ def __init__(self, method, message_id):
3
+ self.message_id: str = message_id
4
+ self.method: str = method
5
+
6
+ def __str__(self):
7
+ return "MessageBase: message_id: %s, method: %s, message_data: %s %s" % (self.message_id, self.method, self.args, self.kwargs)
@@ -0,0 +1,25 @@
1
+ import uuid
2
+
3
+ from callite.rpctypes.message_base import MessageBase
4
+
5
+
6
+ class Request(MessageBase):
7
+ def __init__(self, method: str, client_id:str, message_id = None, *args, **kwargs):
8
+ super(Request, self).__init__(method, message_id)
9
+ self.request_id = message_id if message_id else uuid.uuid4().hex
10
+ self.client_id = client_id
11
+ self.args = args
12
+ self.kwargs = kwargs
13
+
14
+ def set_data(self, data):
15
+ self.data = data
16
+ def payload_json(self):
17
+ return {
18
+ 'request_id': self.request_id,
19
+ 'client_id': self.client_id,
20
+ 'method': self.method,
21
+ 'params': {'args': self.args, 'kwargs': self.kwargs}
22
+ }
23
+
24
+ def __str__(self):
25
+ return "Request: request_id: %s, method: %s" % (self.request_id, self.method)
@@ -0,0 +1,18 @@
1
+ import json
2
+
3
+ from callite.rpctypes.message_base import MessageBase
4
+
5
+
6
+ class Response(MessageBase):
7
+ def __init__(self, method: str, message_id = None):
8
+ super(Response, self).__init__(method, message_id)
9
+ self.data = None
10
+
11
+
12
+ def response_json(self):
13
+ # get the json string of the current instance
14
+ response_data = json.dumps(self).encode('utf-8')
15
+ return response_data
16
+
17
+ def __str__(self):
18
+ return "Response: message_id: %s, response_data: %s" % (self.message_id, self.data)
@@ -0,0 +1,10 @@
1
+ from callite.rpctypes.request import Request
2
+
3
+
4
+ class RPCException(Exception):
5
+
6
+ def __init__(self, message, ex):
7
+ super().__init__(message)
8
+ self.message = message
9
+ self.request: Request | None = None
10
+ self.inner_exception = ex
@@ -0,0 +1,3 @@
1
+ # add rpcserver to all modules
2
+ from .rpc_server import *
3
+ __all__ = ['RPCServer']
@@ -0,0 +1,89 @@
1
+ import json
2
+ import logging
3
+ import threading
4
+ import time
5
+ from types import FunctionType
6
+ from typing import Any, Callable
7
+
8
+ import redis
9
+
10
+ from callite.rpctypes.response import Response
11
+ from callite.shared.redis_connection import RedisConnection
12
+
13
+
14
+ # import pydevd_pycharm
15
+ # pydevd_pycharm.settrace('host.docker.internal', port=4444, stdoutToServer=True, stderrToServer=True)
16
+ # TODO: Check method calls and parameters
17
+ class RPCServer(RedisConnection):
18
+ def __init__(self, conn_url: str, service: str, *args, **kwargs):
19
+ super().__init__(conn_url, service, *args, **kwargs)
20
+ self._registered_methods = {}
21
+ self._xread_groupname = kwargs.get('xread_groupname', 'generic')
22
+
23
+ t = threading.Thread(target=self._subscribe_redis, daemon=True)
24
+ t.start()
25
+ self._logger = logging.getLogger(__name__)
26
+ self._logger.addHandler(logging.StreamHandler())
27
+ self._logger.setLevel(logging.INFO)
28
+
29
+
30
+ def register(self, handler: FunctionType | Callable, method_name: str | None = None) -> Callable:
31
+ method_name = method_name or handler.__name__
32
+ self._registered_methods[method_name] = handler
33
+ return handler
34
+
35
+ def run_forever(self) -> None:
36
+ while self._running: time.sleep(1000000)
37
+
38
+
39
+ def _subscribe_redis(self):
40
+ self._create_redis_group()
41
+ while self._running:
42
+ messages = self._read_messages_from_redis()
43
+ self._process_messages(messages)
44
+
45
+ def _create_redis_group(self):
46
+ try:
47
+ self._rds.xgroup_create(f'{self._queue_prefix}/request/{self._service}', self._xread_groupname, mkstream=True)
48
+ except redis.exceptions.ResponseError as e:
49
+ if "name already exists" not in str(e): raise
50
+
51
+ def _read_messages_from_redis(self):
52
+ messages = self._rds.xreadgroup(self._xread_groupname, self._connection_id, {f'{self._queue_prefix}/request/{self._service}': '>'}, count=1, block=1000, noack=True)
53
+ self._logger.info(f"{len(messages)} messages received from {self._queue_prefix}/request/{self._service}")
54
+ return messages
55
+
56
+ def _process_messages(self, messages):
57
+ for _, message_list in messages:
58
+ for _message in message_list:
59
+ message_id, message_data = _message
60
+ message_data = json.loads(message_data[b'data'])
61
+ self._logger.info(f"Processing message {message_id} with data: {message_data}")
62
+ self._handle_messages(message_data, message_id)
63
+
64
+ def _handle_messages(self, message_data, message_id):
65
+ response = self._call_registered_method(message_data['method'], message_id, message_data['params'])
66
+ request_id = message_data['request_id']
67
+ self._logger.info(f"Response to message {message_id} is {response}")
68
+ payload = json.dumps({'data': response, 'request_id': request_id})
69
+ self._rds.publish(f'{self._queue_prefix}/response/{message_data["client_id"]}', payload)
70
+ self._rds.xack(f'{self._queue_prefix}/request/{self._service}', self._xread_groupname, message_id)
71
+ self._logger.info(f"Processed message {message_id} and response published to {self._queue_prefix}/response/{message_data['request_id']}")
72
+
73
+
74
+ def _call_registered_method(self, method: str, message_id, params: dict) -> Any:
75
+ if method not in self._registered_methods:
76
+ self._logger.warn(f"Method {method} not registered")
77
+ return
78
+ try:
79
+ data = self._registered_methods[method](*params['args'], **params['kwargs'])
80
+
81
+ # TODO: Check why message_id is bytes
82
+ message_id = message_id.decode('utf-8') if isinstance(message_id, bytes) else message_id
83
+ response = Response(self._service, message_id)
84
+ response.data = data
85
+ return response.__dict__
86
+ except Exception as e:
87
+ self._logger.error(e)
88
+ # TODO: log and return exception
89
+ return
@@ -0,0 +1,2 @@
1
+ from .redis_connection import *
2
+ __all__ = ['RedisConnection']
@@ -0,0 +1,22 @@
1
+ import logging
2
+ import uuid
3
+ import redis
4
+ from abc import ABC
5
+
6
+
7
+
8
+ class RedisConnection(ABC):
9
+ def __init__(self, conn_url: str, service: str, *args, **kwargs):
10
+ self._methods = {}
11
+ self._service = service
12
+ self._running = True
13
+ self._running_threads = []
14
+ self._connection_id = uuid.uuid4().hex
15
+ self._queue_prefix = kwargs.get('queue_prefix', '/rpclite')
16
+ self._rds = redis.Redis.from_url(conn_url)
17
+ self._rds.ping()
18
+ self._logger = logging.getLogger(f'RPC{service}')
19
+ self._logger.setLevel(logging.INFO)
20
+
21
+ def close(self) -> None:
22
+ self._running = False
@@ -0,0 +1,20 @@
1
+ [tool.poetry]
2
+ name = "callite"
3
+ version = "0.1.4"
4
+ description = "Slim Redis RPC implementation"
5
+ authors = ["Emrah Gozcu <gozcu@gri.ai>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ redis = "^5.0.3"
12
+
13
+ [tool.poetry.dev-dependencies]
14
+ mypy = "^1.9.0"
15
+ setuptools = "^69.1.1"
16
+ pydevd-pycharm = "~=233.14475.28"
17
+
18
+ [build-system]
19
+ requires = ["poetry-core>=1.0.0"]
20
+ build-backend = "poetry.core.masonry.api"
callite-0.1.4/setup.py ADDED
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+ from setuptools import setup
3
+
4
+ packages = \
5
+ ['callite',
6
+ 'callite.client',
7
+ 'callite.rpctypes',
8
+ 'callite.server',
9
+ 'callite.shared']
10
+
11
+ package_data = \
12
+ {'': ['*']}
13
+
14
+ install_requires = \
15
+ ['redis>=5.0.3,<6.0.0']
16
+
17
+ setup_kwargs = {
18
+ 'name': 'callite',
19
+ 'version': '0.1.4',
20
+ 'description': 'Slim Redis RPC implementation',
21
+ 'long_description': '# RPClite\n\nRPClite is a lightweight Remote Procedure Call (RPC) implementation over Redis, designed to facilitate communication between different components of a distributed system. It minimizes dependencies and offers a simple yet effective solution for decoupling complex systems, thus alleviating potential library conflicts.\n\n## Setting up RPClite\n\nBefore using RPClite, ensure you have a Redis instance running. You can start a Redis server using the default settings or configure it as per your requirements.\n\n## Implementing the Server\n\nTo implement the RPClite server, follow these steps:\n\n1. Import the `RPCService` class from `server.rpc_server`.\n2. Define your main class and initialize the RPC service with the Redis URL and service name.\n3. Register your functions with the RPC service using the `register` decorator.\n4. Run the RPC service indefinitely.\n\nHere\'s an example implementation:\n\n```python\nfrom callite.server import RPCService\n\n\nclass Main:\n def __init__(self):\n self.service = "service"\n self.redis_url = "redis://redis:6379/0"\n self.rpc_service = RPCService(self.redis_url, self.service)\n\n def run(self):\n @self.rpc_service.register\n def healthcheck():\n return "OK"\n\n self.rpc_service.run_forever()\n\n\nif __name__ == "__main__":\n Main().run()\n```\n\n## Calling the Function from Client\n\nOnce the server is set up, you can call functions remotely from the client side. Follow these steps to call functions:\n\n1. Import the `RPCClient` class from `client.rpc_client`.\n2. Define your client class and initialize the RPC client with the Redis URL and service name.\n3. Call the function using the `execute` method of the RPC client.\n4. Optionally, you can pass arguments and keyword arguments to the function.\n\nHere\'s an example client implementation:\n\n```python\nimport time\nfrom callite.client.rpc_client import RPCClient\n\n\nclass Healthcheck():\n def __init__(self):\n self.status = "OK"\n self.r = RPCClient("redis://redis:6379/0", "service")\n\n def get_status(self):\n start = time.perf_counter()\n self.status = self.r.execute(\'healthcheck\')\n end = time.perf_counter()\n print(f"Healthcheck took {end - start:0.4f} seconds")\n return self.status\n\n def check(self):\n return self.get_status()\n\n\nif __name__ == "__main__":\n Healthcheck().check()\n```\n\nYou can pass arguments and keyword arguments to the `execute` method as follows:\n\n```python\nself.status = self.r.execute(\'healthcheck\', [True], {\'a\': 1, \'b\': 2})\n```\n\nThis setup allows for efficient communication between components of a distributed system, promoting modularity and scalability.\n```',
22
+ 'author': 'Emrah Gozcu',
23
+ 'author_email': 'gozcu@gri.ai',
24
+ 'maintainer': None,
25
+ 'maintainer_email': None,
26
+ 'url': None,
27
+ 'packages': packages,
28
+ 'package_data': package_data,
29
+ 'install_requires': install_requires,
30
+ 'python_requires': '>=3.10,<4.0',
31
+ }
32
+
33
+
34
+ setup(**setup_kwargs)