From 4d6cbc08640da870c9349058e4862343e0f644dc Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Sun, 26 Oct 2014 13:28:14 -0400 Subject: [PATCH] initial commit --- .gitignore | 2 ++ LICENSE | 22 ++++++++++++ MANIFEST.in | 4 +++ README.md | 0 bin/greenrpc-client | 25 +++++++++++++ bin/greenrpc-server | 24 +++++++++++++ greenrpc/__init__.py | 4 +++ greenrpc/base.py | 83 ++++++++++++++++++++++++++++++++++++++++++++ greenrpc/client.py | 29 ++++++++++++++++ greenrpc/error.py | 2 ++ greenrpc/server.py | 31 +++++++++++++++++ requirements.txt | 2 ++ setup.cfg | 2 ++ setup.py | 30 ++++++++++++++++ 14 files changed, 260 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100755 bin/greenrpc-client create mode 100755 bin/greenrpc-server create mode 100644 greenrpc/__init__.py create mode 100644 greenrpc/base.py create mode 100644 greenrpc/client.py create mode 100644 greenrpc/error.py create mode 100644 greenrpc/server.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..619fb97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.py[co] +*.egg-info diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70088c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Brett Langdon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9c46a97 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.* LICENSE setup.py setup.cfg +recursive-include greenrpc *.py +global-exclude *.pyc +global-exclude *.pyo diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bin/greenrpc-client b/bin/greenrpc-client new file mode 100755 index 0000000..f009d6a --- /dev/null +++ b/bin/greenrpc-client @@ -0,0 +1,25 @@ +#!/usr/bin/env python +import argparse + +from greenrpc import TCP_SERVER_DEFAULT_PORT +from greenrpc.client import TCPClient + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Start a new GreenRPC TCP Server") + parser.add_argument("method", metavar="", type=str, + help="The remote method to call") + parser.add_argument("args", metavar="", nargs="*", type=str, + help="Arguments to send for the remote method call") + + default_connect = "127.0.0.1:%s" % (TCP_SERVER_DEFAULT_PORT, ) + parser.add_argument("--connect", dest="connect", type=str, default=default_connect, + help="
: of the server to connect to(default: %s)" % (default_connect, )) + + parser.add_argument("--debug", dest="debug", action="store_true", default=False, + help="Whether or not to show the full result") + + args = parser.parse_args() + address, _, port = args.connect.partition(":") + client = TCPClient(connect=(address, int(port))) + result = client.call(args.method, args.args, debug=args.debug) + print result diff --git a/bin/greenrpc-server b/bin/greenrpc-server new file mode 100755 index 0000000..e5c6713 --- /dev/null +++ b/bin/greenrpc-server @@ -0,0 +1,24 @@ +#!/usr/bin/env python +import argparse + +from greenrpc import TCP_SERVER_DEFAULT_PORT +from greenrpc.server import TCPServer + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Start a new GreenRPC TCP Server") + parser.add_argument("module", metavar="", type=str, + help="Python module to expose for the RPC Server") + default_bind = "127.0.0.1:%s" % (TCP_SERVER_DEFAULT_PORT, ) + parser.add_argument("--bind", dest="bind", type=str, default=default_bind, + help="
: to bind the server to (default: %s)" % (default_bind, )) + parser.add_argument("--spawn", dest="spawn", type=int, default=4, + help="number of greenlets to spawn (default: 4)") + + args = parser.parse_args() + address, _, port = args.bind.partition(":") + server = TCPServer(args.module, bind=(address, int(port)), spawn=args.spawn) + try: + server.serve_forever() + except KeyboardInterrupt: + print "Stopping GreenRPC Server" + server.stop() diff --git a/greenrpc/__init__.py b/greenrpc/__init__.py new file mode 100644 index 0000000..343a9f2 --- /dev/null +++ b/greenrpc/__init__.py @@ -0,0 +1,4 @@ +__version__ = "0.1.0" + +TCP_SERVER_DEFAULT_PORT = 3434 +HTTP_SERVER_DEFAULT_PORT = 3435 diff --git a/greenrpc/base.py b/greenrpc/base.py new file mode 100644 index 0000000..1e1fae7 --- /dev/null +++ b/greenrpc/base.py @@ -0,0 +1,83 @@ +import time +import types + +import msgpack + + +class BaseServer(object): + SOCKET_BUFFER_SIZE = 1024 + + def __init__(self, services): + if isinstance(services, (dict, types.ModuleType)): + self.services = services + elif isinstance(services, basestring): + self.services = __import__(services) + else: + raise TypeError("First argument to BaseServer.__init__ must be a dict or a string") + + self.packer = msgpack.Packer() + + def unpack_requests(self, sock): + unpacker = msgpack.Unpacker() + while True: + data = sock.recv(self.SOCKET_BUFFER_SIZE) + if not data: + break + unpacker.feed(data) + for request in unpacker: + yield request + + def pack_result(self, result): + return self.packer.pack(result) + + def handle_request(self, request): + start_time = time.time() + req_method = request.get("method") + req_args = request.get("args", []) + + result = { + "id": request.get("id"), + "results": None, + } + + if not req_method: + result["error"] = "No request method was provided" + elif not hasattr(self.services, req_method): + result["error"] = "Unknown request method '%s'" % (req_method, ) + else: + try: + result["results"] = getattr(self.services, req_method)(*req_args) + except Exception, e: + result["error"] = e.message + + result["run_time"] = (time.time() - start_time) * 1000.0 + return result + + +class BaseClient(object): + SOCKET_BUFFER_SIZE = 1024 + + def __init__(self): + self.unpacker = msgpack.Unpacker() + self.packer = msgpack.Packer() + self.id = 0 + + def pack_request(self, id, method, args=[]): + return self.packer.pack({ + "id": id, + "method": method, + "args": args, + }) + + def unpack_results(self, sock): + while True: + data = sock.recv(self.SOCKET_BUFFER_SIZE) + if not data: + break + self.unpacker.feed(data) + return self.unpacker.next() + + def __getattr__(self, method): + def wrapper(*args): + return self.call(method, args) + return wrapper diff --git a/greenrpc/client.py b/greenrpc/client.py new file mode 100644 index 0000000..3b455f7 --- /dev/null +++ b/greenrpc/client.py @@ -0,0 +1,29 @@ +from gevent.socket import socket + +from greenrpc import TCP_SERVER_DEFAULT_PORT +from greenrpc.base import BaseClient +from greenrpc.error import RPCException + + +class TCPClient(BaseClient): + def __init__(self, connect=("127.0.0.1", TCP_SERVER_DEFAULT_PORT)): + super(TCPClient, self).__init__() + self.connection = socket() + self.connection.connect(connect) + self.fp = self.connection.makefile() + + def call(self, method, args=[], debug=False): + self.id += 1 + request = self.pack_request(self.id, method, args) + self.fp.write(request) + self.fp.flush() + results = self.unpack_results(self.connection) + if debug: + return results + if results.get("error"): + raise RPCException(results["error"]) + return results.get("results") + + +class HTTPClient(BaseClient): + pass diff --git a/greenrpc/error.py b/greenrpc/error.py new file mode 100644 index 0000000..f63f7ae --- /dev/null +++ b/greenrpc/error.py @@ -0,0 +1,2 @@ +class RPCException(Exception): + pass diff --git a/greenrpc/server.py b/greenrpc/server.py new file mode 100644 index 0000000..27908c4 --- /dev/null +++ b/greenrpc/server.py @@ -0,0 +1,31 @@ +import contextlib + +from gevent.pywsgi import WSGIServer +from gevent.server import StreamServer + +from greenrpc import TCP_SERVER_DEFAULT_PORT, HTTP_SERVER_DEFAULT_PORT +from greenrpc.base import BaseServer + + +class TCPServer(StreamServer, BaseServer): + def __init__(self, services, bind=("127.0.0.1", TCP_SERVER_DEFAULT_PORT), spawn=1): + StreamServer.__init__(self, bind, spawn=spawn) + BaseServer.__init__(self, services) + + def handle(self, socket, address): + with contextlib.closing(socket) as sock: + with contextlib.closing(sock.makefile()) as fp: + for request in self.unpack_requests(sock): + result = self.handle_request(request) + fp.write(self.pack_result(result)) + fp.flush() + + +class WSGIServer(WSGIServer, BaseServer): + def __init__(self, services, bind=("127.0.0.1", HTTP_SERVER_DEFAULT_PORT)): + WSGIServer.__init__(self, bind) + BaseServer.__init__(self, services) + + def application(self, environ, start_response): + start_response("200 OK", [("Content-Type", "text/plain")]) + return ["Hello, Worlds"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b86f88e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +gevent==1.0.1 +msgpack-python==0.4.2 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..df731f4 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +from greenrpc import __version__ + +with open("./requirements.txt") as fp: + requirements = fp.read() + requirements = requirements.split("\n") + +setup( + name="greenrpc", + version=__version__, + description="msgpack TCP & HTTP RPC Servers written with gevent", + author="Brett Langdon", + author_email="brett@blangdon.com", + url="https://github.com/brettlangdon/greenrpc", + packages=find_packages(), + license="MIT", + scripts=["bin/greenrpc-server", "bin/greenrpc-client"], + install_requires=requirements, + classifiers=[ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "License :: OSI Approved :: MIT License", + "Topic :: Utilities", + ] +)