19 |
20 |
Hello {{! userInfo.username }}
21 |
22 |
23 | Your email is {{! userInfo.email }}
24 |
25 |
26 |
27 | We last saw you {{! userInfo.last_seen }}
28 |
29 |
30 |
31 |
32 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/gemstone/discovery/cache.py:
--------------------------------------------------------------------------------
1 | import time
2 | import threading
3 |
4 |
5 | class DummyCache(object):
6 | """
7 | Dummy remote service cache. Always returns ``None`` which triggers a
8 | service registry query.
9 |
10 | Example usage:
11 |
12 | ::
13 |
14 | class MyMicroService(MicroService):
15 | # ...
16 | service_registry_cache = DummyCache()
17 | # ...
18 |
19 | """
20 | def __init__(self):
21 | pass
22 |
23 | def add_entry(self, name, remote_service):
24 | pass
25 |
26 | def get_entry(self, name):
27 | pass
28 |
29 |
30 | class CacheEntry(object):
31 | def __init__(self, name, remote_service):
32 | self.created = time.time()
33 | self.name = name
34 | self.remote_service = remote_service
35 |
36 | def is_still_valid(self, max_age):
37 | return (time.time() - self.created) < max_age
38 |
39 |
40 | class ServiceDiscoveryCache(object):
41 | def __init__(self, cache_lifetime_in_seconds):
42 | """
43 | Service discovery cache that keeps entries in memory for
44 | a constant period of time.
45 |
46 | Example usage:
47 |
48 | ::
49 |
50 | class MyMicroService(MicroService):
51 | # ...
52 | service_registry_cache = ServiceDiscoveryCache(60) # keeps entries for one minute
53 | # ...
54 |
55 | :param cache_lifetime_in_seconds: int that specifies the cache entry lifetime
56 | """
57 |
58 | self.ttl = cache_lifetime_in_seconds
59 | self.container = {}
60 | self.container_lock = threading.Lock()
61 |
62 | def add_entry(self, name, remote_service):
63 | with self.container_lock:
64 | self.container.setdefault(name, [])
65 | self.container[name].append(CacheEntry(name, remote_service))
66 |
67 | def get_entry(self, name):
68 | self.expire_entries()
69 |
70 | with self.container_lock:
71 | for entry in self.container.get(name, []):
72 | if entry.is_still_valid(self.ttl):
73 | return entry
74 |
75 | def expire_entries(self):
76 | with self.container_lock:
77 | for _, entries in self.container.items():
78 | i = 0
79 | while i < len(entries):
80 | current = entries[i]
81 | if current.is_still_valid(self.ttl):
82 | i += 1
83 | else:
84 | del entries[i]
85 |
--------------------------------------------------------------------------------
/gemstone/event/transport/redis_transport.py:
--------------------------------------------------------------------------------
1 | import urllib.parse
2 |
3 | import simplejson as json
4 |
5 | try:
6 | import redis
7 | except ImportError:
8 | redis = None
9 |
10 | from gemstone.event.transport.base import BaseEventTransport
11 |
12 |
13 | class RedisEventTransport(BaseEventTransport):
14 | def __init__(self, redis_url):
15 | """
16 | Event transport that uses a Redis server as message transport by using
17 | the PUBSUB mechanism.
18 |
19 | :param redis_url: A string that specifies the network location of the redis server:
20 | - ``redis://[:password@]hostaddr:port/dbnumber`` (plaintext)
21 | - ``rediss://[:password@]hostaddr:port/dbnumber`` (over TLS)
22 | - ``unix://[:password@]/path/to/socket?db=dbnumber`` (Unix socket)
23 |
24 | """
25 | if not redis:
26 | raise RuntimeError("RedisEventTransport requires 'redis' to run")
27 |
28 | super(RedisEventTransport, self).__init__()
29 | conn_details = urllib.parse.urlparse(redis_url)
30 | if conn_details.scheme not in ("redis", "rediss", "unix"):
31 | raise ValueError(
32 | "Invalid redis url: scheme '{}' not allowed".format(conn_details.scheme))
33 |
34 | self.connection_pool = redis.ConnectionPool.from_url(redis_url)
35 | self.handlers = {}
36 |
37 | def get_pubsub(self):
38 | conn = self.get_redis_connection()
39 | pubsub = conn.pubsub(ignore_subscribe_messages=True)
40 | return pubsub
41 |
42 | def get_redis_connection(self):
43 | return redis.StrictRedis(connection_pool=self.connection_pool)
44 |
45 | def start_accepting_events(self):
46 | pubsub = self.get_pubsub()
47 | pubsub.subscribe(*tuple(self.handlers.keys()))
48 |
49 | for message in pubsub.listen():
50 | event_name = message["channel"].decode()
51 | event_data = message["data"].decode()
52 |
53 | self.on_event_received(event_name, json.loads(event_data))
54 |
55 | def register_event_handler(self, handler_func, handled_event_name):
56 | self.handlers[handled_event_name] = handler_func
57 |
58 | def emit_event(self, event_name, event_body):
59 | conn = self.get_redis_connection()
60 | conn.publish(event_name, json.dumps(event_body))
61 |
62 | def on_event_received(self, event_name, event_body):
63 | handler = self.handlers.get(event_name, None)
64 | if not handler:
65 | return
66 | self.run_on_main_thread(handler, (event_body,), {})
67 |
--------------------------------------------------------------------------------
/docs/topics/configuration.rst:
--------------------------------------------------------------------------------
1 | .. _configuration-tips:
2 |
3 | Configurable features
4 | =====================
5 |
6 | In the context of this framework, configurables are
7 | entities that designate what properties of the microservice
8 | can be dynamically set and configurators are strategies
9 | that, on service startup, collects the required
10 | properties from the environment.
11 |
12 | Currently, the available confugurators are:
13 |
14 | - :py:class:`gemstone.config.configurator.CommandLineConfigurator` - collects values from
15 | the command line arguments
16 |
17 | In order to specify configurables for the microservice, you have to provide
18 | set the :py:data:`gemstone.MicroService.configurables` attribute to a list of
19 | :py:class:`Configurable` objects.
20 |
21 | Configurators are specified in the :py:data:`gemstone.MicroService.configurators` attribute.
22 | On service startup, each configurator tries to extract the required values from the environment in
23 | the order they are defined.
24 |
25 |
26 | In order to trigger the configurators, you need to explicitly call the
27 | :py:meth:`gemstone.MicroService.configure` method before calling
28 | :py:meth:`gemstone.MicroService.start`
29 |
30 | Defining configurators
31 | ----------------------
32 |
33 | Configurators are defined in the :py:attr:`gemstone.MicroService.configurators` class attribute.
34 |
35 | ::
36 |
37 | class ExampleService(gemstone.MicroService):
38 | # ...
39 | configurators = [
40 | gemstone.config.CommandLineConfigurator()
41 | ]
42 | # ...
43 |
44 | Defining configurables
45 | ----------------------
46 |
47 | Configurables are defined in the :py:attr:`gemstone.MicroService.configurables` class attribute.
48 |
49 | ::
50 |
51 | class ExampleService(gemstone.MicroService):
52 | # ...
53 | configurables = [
54 | gemstone.config.Configurable("port", template=lambda x: int(x)),
55 | gemstone.config.Configurable("discovery_strategies",
56 | template=lambda x: [RedisDiscoveryStrategy(a) for a in x.split(",")]),
57 | gemstone.config.Configurable("host"),
58 | gemstone.config.Configurable("accessible_at"),
59 | ]
60 | # ...
61 |
62 | In the example above, we defined 4 configurables:
63 |
64 | - ``gemstone.config.Configurable("port", template=lambda x: int(x))`` the ``--port`` command-line
65 | argument will be casted to ``int`` and assigned to ``gemstone.MicroService.port``
66 | - ``gemstone.config.Configurable("host")`` the ``--host`` command-line
67 | argument assigned to ``gemstone.MicroService.host``
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import re
3 | import sys
4 | from setuptools import setup, find_packages
5 |
6 | # Check the Python version. Currently only 3.4+ is supported
7 |
8 | if sys.version_info < (3, 4):
9 | sys.exit("Supported only for Python 3.4 or newer.")
10 |
11 |
12 | # Utility functions
13 |
14 | def read_dependencies(req_file):
15 | with open(req_file) as req:
16 | return [line.strip() for line in req]
17 |
18 |
19 | def get_file_content(filename):
20 | with open(filename) as f:
21 | return f.read()
22 |
23 |
24 | def get_meta_attr_from_string(meta_attr, content):
25 | result = re.search("{attrname}\s*=\s*['\"]([^'\"]+)['\"]".format(attrname=meta_attr), content)
26 | if not result:
27 | raise RuntimeError("Unable to extract {}".format(meta_attr))
28 | return result.group(1)
29 |
30 |
31 | # Metadata
32 |
33 | CLASSIFIERS = """
34 | Development Status :: 3 - Alpha
35 | License :: OSI Approved :: MIT License
36 | Operating System :: OS Independent
37 | Topic :: Utilities
38 | Programming Language :: Python :: 3.4
39 | Programming Language :: Python :: 3.5
40 | Programming Language :: Python :: 3.6
41 | """
42 | URL = "https://github.com/vladcalin/gemstone"
43 | KEYWORDS = "microservice service gemstone jsonrpc rpc http asynchronous async tornado"
44 | DESCRIPTION = "Build microservices with Python"
45 | LICENSE = "MIT"
46 |
47 | module_content = get_file_content(os.path.join("gemstone", "__init__.py"))
48 |
49 | readme = get_file_content("README.rst")
50 |
51 | setup(
52 | # project metadata
53 | name="gemstone",
54 | version=get_meta_attr_from_string("__version__", module_content),
55 | license=LICENSE,
56 |
57 | author=get_meta_attr_from_string("__author__", module_content),
58 | author_email=get_meta_attr_from_string("__email__", module_content),
59 |
60 | maintainer=get_meta_attr_from_string("__author__", module_content),
61 | maintainer_email=get_meta_attr_from_string("__email__", module_content),
62 |
63 | long_description=readme,
64 | description=DESCRIPTION,
65 | keywords=KEYWORDS.split(),
66 | classifiers=[x.strip() for x in CLASSIFIERS.split("\n") if x != ""],
67 | url=URL,
68 |
69 | zip_safe=False,
70 |
71 | # packages
72 | packages=find_packages(),
73 | include_package_data=True,
74 |
75 | # tests
76 | test_suite="tests",
77 | test_requires="pytest",
78 |
79 | install_requires=[
80 | "tornado",
81 | "simplejson",
82 | "click"
83 | ],
84 |
85 | extras_require={
86 | "rabbitmq": ["pika"],
87 | "redis": ["redis"]
88 | }
89 | )
90 |
--------------------------------------------------------------------------------
/docs/reference/gemstone.core.rst:
--------------------------------------------------------------------------------
1 | The gemstone.core module
2 | ========================
3 |
4 |
5 | .. automodule:: gemstone.core
6 |
7 | The gemstone.core.MicroService class
8 | ------------------------------------
9 |
10 | .. autoclass:: gemstone.core.MicroService
11 |
12 | .. autoattribute:: gemstone.core.MicroService.name
13 | .. autoattribute:: gemstone.core.MicroService.host
14 | .. autoattribute:: gemstone.core.MicroService.port
15 | .. autoattribute:: gemstone.core.MicroService.accessible_at
16 | .. autoattribute:: gemstone.core.MicroService.endpoint
17 | .. autoattribute:: gemstone.core.MicroService.template_dir
18 | .. autoattribute:: gemstone.core.MicroService.static_dirs
19 | .. autoattribute:: gemstone.core.MicroService.extra_handlers
20 | .. autoattribute:: gemstone.core.MicroService.plugins
21 | .. autoattribute:: gemstone.core.MicroService.discovery_strategies
22 | .. autoattribute:: gemstone.core.MicroService.service_registry_ping_interval
23 | .. autoattribute:: gemstone.core.MicroService.periodic_tasks
24 | .. autoattribute:: gemstone.core.MicroService.event_transports
25 | .. autoattribute:: gemstone.core.MicroService.configurables
26 | .. autoattribute:: gemstone.core.MicroService.configurators
27 | .. autoattribute:: gemstone.core.MicroService.max_parallel_blocking_tasks
28 |
29 | Can be called
30 | """""""""""""
31 |
32 | .. automethod:: gemstone.core.MicroService.get_service
33 | .. automethod:: gemstone.core.MicroService.emit_event
34 | .. automethod:: gemstone.core.MicroService.get_io_loop
35 | .. automethod:: gemstone.core.MicroService.get_executor
36 | .. automethod:: gemstone.core.MicroService.start
37 | .. automethod:: gemstone.core.MicroService.configure
38 | .. automethod:: gemstone.core.MicroService.register_plugin
39 | .. automethod:: gemstone.core.MicroService.get_plugin
40 | .. automethod:: gemstone.core.MicroService.start_thread
41 | .. automethod:: gemstone.core.MicroService.make_tornado_app
42 |
43 | Can be overridden
44 | """""""""""""""""
45 |
46 | .. automethod:: gemstone.core.MicroService.get_logger
47 | .. automethod:: gemstone.core.MicroService.authenticate_request
48 | .. automethod:: gemstone.core.MicroService.on_service_start
49 |
50 | The gemstone.core.Container class
51 | ---------------------------------
52 |
53 |
54 | .. autoclass:: gemstone.core.Container
55 | :members:
56 |
57 | .. py:attribute:: microservice
58 |
59 | Decorators
60 | ----------
61 |
62 | .. autofunction:: gemstone.core.exposed_method
63 | .. autofunction:: gemstone.core.event_handler
64 |
65 |
--------------------------------------------------------------------------------
/gemstone/event/transport/base.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class BaseEventTransport(ABC):
5 | """
6 | Base class for defining event transports.
7 |
8 | The basic workflow would be the following:
9 |
10 | - the handlers are registered with the :py:meth:`BaseEventTransport.register_event_handler`
11 | method
12 | - the :py:meth:`BaseEventTransport.start_accepting_events` is invoked
13 | - for each incoming event, call :py:meth:`BaseEventTransport.on_event_received`
14 | whose responsibility is to invoke the proper handler function (recommended
15 | to use the ``run_on_main_thread`` method)
16 |
17 | """
18 |
19 | def __init__(self):
20 | self.microservice = None
21 |
22 | @abstractmethod
23 | def register_event_handler(self, handler_func, handled_event_name):
24 | """
25 | Registers a function to handle all events of type ``handled_event_name``
26 |
27 | :param handler_func: the handler function
28 | :param handled_event_name: the handled event type
29 | """
30 | pass
31 |
32 | @abstractmethod
33 | def start_accepting_events(self):
34 | """
35 | Starts accepting and handling events.
36 | """
37 | pass
38 |
39 | @abstractmethod
40 | def on_event_received(self, event_name, event_body):
41 | """
42 | Handles generic event. This function should treat every event that is received
43 | with the designated handler function.
44 |
45 | :param event_name: the name of the event to be handled
46 | :param event_body: the body of the event to be handled
47 | :return:
48 | """
49 | pass
50 |
51 | @abstractmethod
52 | def emit_event(self, event_name, event_body):
53 | """
54 | Emits an event of type ``event_name`` with the ``event_body`` content using the current
55 | event transport.
56 |
57 | :param event_name:
58 | :param event_body:
59 | :return:
60 | """
61 | pass
62 |
63 | def set_microservice(self, microservice):
64 | """
65 | Used by the microservice instance to send reference to itself. Do not override this.
66 | """
67 | self.microservice = microservice
68 |
69 | def run_on_main_thread(self, func, args=None, kwargs=None):
70 | """
71 | Runs the ``func`` callable on the main thread, by using the provided microservice
72 | instance's IOLoop.
73 |
74 | :param func: callable to run on the main thread
75 | :param args: tuple or list with the positional arguments.
76 | :param kwargs: dict with the keyword arguments.
77 | :return:
78 | """
79 | if not args:
80 | args = ()
81 | if not kwargs:
82 | kwargs = {}
83 | self.microservice.get_io_loop().add_callback(func, *args, **kwargs)
84 |
--------------------------------------------------------------------------------
/tests/events/test_events.py:
--------------------------------------------------------------------------------
1 | import unittest.mock
2 |
3 | import pytest
4 |
5 | from gemstone.core import MicroService, event_handler
6 | from gemstone.event.transport import BaseEventTransport
7 |
8 |
9 | class MockedEventTransport(BaseEventTransport):
10 | register_event_handler = unittest.mock.MagicMock()
11 | on_event_received = unittest.mock.MagicMock()
12 | emit_event = unittest.mock.MagicMock()
13 | start_accepting_events = unittest.mock.MagicMock()
14 |
15 |
16 | class SemiMockedEventTransport(BaseEventTransport):
17 | def __init__(self):
18 | self.handlers = {}
19 |
20 | def start_accepting_events(self):
21 | pass
22 |
23 | def register_event_handler(self, handler_func, handled_event_name):
24 | self.handlers[handled_event_name] = handler_func
25 |
26 | def on_event_received(self, event_name, event_body):
27 | self.handlers[event_name](event_body)
28 |
29 | def emit_event(self, event_name, event_body):
30 | pass
31 |
32 |
33 | mocked_transport = MockedEventTransport()
34 |
35 |
36 | class TestServicePublisher(MicroService):
37 | name = "test"
38 |
39 | event_transports = [
40 | mocked_transport
41 | ]
42 |
43 | handler = unittest.mock.MagicMock()
44 | # it is like a magic mock that is decorated
45 | handler = event_handler("test")(handler)
46 |
47 |
48 | def test_event_handler_discovery():
49 | service = TestServicePublisher()
50 | service._gather_event_handlers()
51 |
52 | assert service.event_handlers == {"test": service.handler}
53 |
54 |
55 | def test_transport_init():
56 | service = TestServicePublisher()
57 | service._gather_event_handlers()
58 | service._initialize_event_handlers()
59 |
60 | mocked_transport.register_event_handler.assert_has_calls([
61 | unittest.mock.call(service.handler, "test")
62 | ])
63 |
64 |
65 | def test_transport_start_in_threads():
66 | service = TestServicePublisher()
67 | service.start_thread = unittest.mock.MagicMock()
68 |
69 | service._start_event_handlers()
70 |
71 | service.start_thread.assert_has_calls([
72 | unittest.mock.call(target=mocked_transport.start_accepting_events, args=(), kwargs={})
73 | ])
74 |
75 |
76 | def test_emit_event():
77 | service = TestServicePublisher()
78 |
79 | service.emit_event("testing", "test_value")
80 | service.emit_event("testing2", "test_value2")
81 |
82 | mocked_transport.emit_event.assert_has_calls([
83 | unittest.mock.call("testing", "test_value"),
84 | unittest.mock.call("testing2", "test_value2")
85 | ])
86 |
87 |
88 | def test_receive_event():
89 | transport = SemiMockedEventTransport()
90 | TestServicePublisher.event_transports = [transport]
91 | service = TestServicePublisher()
92 | service._gather_event_handlers()
93 | service._initialize_event_handlers()
94 | service._start_event_handlers()
95 |
96 | transport.on_event_received("test", "hello")
97 |
98 | service.handler.assert_has_calls([
99 | unittest.mock.call("hello")
100 | ])
101 |
--------------------------------------------------------------------------------
/tests/configuration/test_cmd_configuration.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import pytest
3 |
4 | from gemstone.config import CommandLineConfigurator, Configurable
5 |
6 |
7 | def test_configurator_command_line_no_configurable(monkeypatch):
8 | configurables = [
9 | Configurable("a"),
10 | Configurable("b"),
11 | Configurable("c")
12 | ]
13 |
14 | monkeypatch.setattr(sys, "argv", [sys.argv[0]])
15 |
16 | configurator = CommandLineConfigurator()
17 | for c in configurables:
18 | configurator.register_configurable(c)
19 |
20 | configurator.load()
21 | assert configurator.get("a") is None
22 | assert configurator.get("b") is None
23 | assert configurator.get("c") is None
24 | assert configurator.get("d") is None
25 |
26 |
27 | def test_configurator_command_line_one_configurable(monkeypatch):
28 | configurables = [
29 | Configurable("a"),
30 | Configurable("b"),
31 | Configurable("c")
32 | ]
33 |
34 | monkeypatch.setattr(sys, "argv", [sys.argv[0], "--a=1"])
35 |
36 | configurator = CommandLineConfigurator()
37 |
38 | for c in configurables:
39 | configurator.register_configurable(c)
40 | configurator.load()
41 |
42 | assert configurator.get("a") == "1"
43 | assert configurator.get("b") is None
44 | assert configurator.get("c") is None
45 | assert configurator.get("d") is None
46 |
47 |
48 | def test_configurator_command_line_two_configurables(monkeypatch):
49 | configurables = [
50 | Configurable("a"),
51 | Configurable("b"),
52 | Configurable("c")
53 | ]
54 |
55 | monkeypatch.setattr(sys, "argv", [sys.argv[0], "--a=1", "--b", "3"])
56 |
57 | configurator = CommandLineConfigurator()
58 |
59 | for c in configurables:
60 | configurator.register_configurable(c)
61 | configurator.load()
62 |
63 | assert configurator.get("a") == "1"
64 | assert configurator.get("b") == "3"
65 | assert configurator.get("c") is None
66 | assert configurator.get("d") is None
67 |
68 |
69 | def test_configurator_command_line_three_configurables(monkeypatch):
70 | configurables = [
71 | Configurable("a"),
72 | Configurable("b"),
73 | Configurable("c")
74 | ]
75 |
76 | monkeypatch.setattr(sys, "argv", [sys.argv[0], "--a=1", "--b", "3", "--c=2"])
77 |
78 | configurator = CommandLineConfigurator()
79 |
80 | for c in configurables:
81 | configurator.register_configurable(c)
82 | configurator.load()
83 |
84 | assert configurator.get("a") == "1"
85 | assert configurator.get("b") == "3"
86 | assert configurator.get("c") == "2"
87 | assert configurator.get("d") is None
88 |
89 |
90 | def test_configurator_command_line_three_configurables_one_extra_param(monkeypatch):
91 | configurables = [
92 | Configurable("a"),
93 | Configurable("b"),
94 | Configurable("c")
95 | ]
96 |
97 | monkeypatch.setattr(sys, "argv", [sys.argv[0], "--a=1", "--b", "3", "--c=2", "--d=4"])
98 |
99 | configurator = CommandLineConfigurator()
100 |
101 | for c in configurables:
102 | configurator.register_configurable(c)
103 |
104 | with pytest.raises(SystemExit):
105 | # unrecognized parameter: --d
106 | configurator.load()
107 |
--------------------------------------------------------------------------------
/gemstone/config/configurator.py:
--------------------------------------------------------------------------------
1 | import abc
2 | import argparse
3 |
4 |
5 | class BaseConfigurator(abc.ABC):
6 | """
7 | Base class for defining configurators. A configurator is a class that, starting
8 | from a set of name-configurable pairs, depending on the configurables' options
9 | and the environment, builds a configuration for the application.
10 |
11 | """
12 |
13 | def __init__(self):
14 | self.configurables = []
15 |
16 | @abc.abstractmethod
17 | def load(self):
18 | """
19 | Loads the configuration for the application
20 | """
21 | pass
22 |
23 | @abc.abstractmethod
24 | def get(self, name):
25 | """
26 | Gets the extracted value for the specified name, if available. If no value could
27 | be loaded for the specified name, ``None`` must be returned.
28 | """
29 | pass
30 |
31 | def register_configurable(self, configurable):
32 | """
33 | Registers a configurable instance with this configurator
34 |
35 | :param configurable: a :py:class:`Configurable` instance
36 | """
37 | self.configurables.append(configurable)
38 |
39 | def get_configurable_by_name(self, name):
40 | """
41 | Returns the registered configurable with the specified name or ``None`` if no
42 | such configurator exists.
43 | """
44 | l = [c for c in self.configurables if c.name == name]
45 | if l:
46 | return l[0]
47 |
48 | def __repr__(self):
49 | return "<{}>".format(self.__class__.__name__)
50 |
51 | def __str__(self):
52 | return repr(self)
53 |
54 |
55 | class CommandLineConfigurator(BaseConfigurator):
56 | """
57 | Configurator that collects values from command line arguments.
58 | For each registered configurable, will attempt to get from command line
59 | the value designated by the argument ``--name`` where ``name`` is the name of the
60 | configurable.
61 |
62 | Example
63 |
64 | For the configurables
65 |
66 | - Configurable("a")
67 | - Configurable("b")
68 | - Configurable("c")
69 |
70 | the following command line interface will be exposed
71 |
72 | ::
73 |
74 | usage: service.py [-h] [--a A] [--b B] [--c C]
75 |
76 | optional arguments:
77 | -h, --help show this help message and exit
78 | --a A
79 | --b B
80 | --c C
81 |
82 | The ``service.py`` can be called like this
83 |
84 | ::
85 |
86 | python service.py --a=1 --b=2 --c=3
87 |
88 |
89 | """
90 |
91 | def __init__(self):
92 | super(CommandLineConfigurator, self).__init__()
93 | self.args = None
94 |
95 | def load(self):
96 | parser = argparse.ArgumentParser()
97 | for configurable in self.configurables:
98 | parser.add_argument("--" + configurable.name)
99 | self.args = parser.parse_args()
100 |
101 | def get(self, name):
102 | configurable = self.get_configurable_by_name(name)
103 | if not configurable:
104 | return None
105 |
106 | value = getattr(self.args, name, None)
107 | if not value:
108 | return None
109 |
110 | configurable.set_value(value)
111 | return configurable.get_final_value()
112 |
--------------------------------------------------------------------------------
/gemstone/event/transport/rabbitmq.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | try:
4 | import pika
5 | except ImportError:
6 | pika = None
7 |
8 | from gemstone.event.transport.base import BaseEventTransport
9 |
10 |
11 | class RabbitMqEventTransport(BaseEventTransport):
12 | EXCHANGE_PREFIX_BROADCAST = "gemstone.broadcast."
13 |
14 | def __init__(self, host="127.0.0.1", port=5672, username="", password="", **connection_options):
15 | """
16 | Event transport via RabbitMQ server.
17 |
18 | :param host: ipv4 or hostname
19 | :param port: the port where the server listens
20 | :param username: username used for authentication
21 | :param password: password used for authentication
22 | :param connection_options: extra arguments that will be used in
23 | :py:class:`pika.BlockingConnection` initialization.
24 | """
25 | if not pika:
26 | raise RuntimeError("RabbitMqEventTransport requires 'pika' to run")
27 |
28 | super(RabbitMqEventTransport, self).__init__()
29 | self._handlers = {}
30 |
31 | self.connection = pika.BlockingConnection(
32 | pika.ConnectionParameters(
33 | host=host, port=port,
34 | credentials=pika.PlainCredentials(username=username, password=password),
35 | **connection_options
36 | )
37 | )
38 | self.channel = self.connection.channel()
39 |
40 | def register_event_handler(self, handler_func, handled_event_name):
41 | self._handlers[handled_event_name] = handler_func
42 |
43 | def start_accepting_events(self):
44 | for event_name, event_handler in self._handlers.items():
45 | # prepare broadcast queues
46 | current_exchange_name = self.EXCHANGE_PREFIX_BROADCAST + event_name
47 | self.channel.exchange_declare(
48 | exchange=current_exchange_name,
49 | type="fanout"
50 | )
51 | result = self.channel.queue_declare(exclusive=True)
52 | queue_name = result.method.queue
53 |
54 | self.channel.queue_bind(exchange=current_exchange_name, queue=queue_name)
55 | self.channel.basic_consume(self._callback, queue=queue_name, no_ack=True)
56 |
57 | self.channel.start_consuming()
58 |
59 | def _callback(self, channel, method, properties, body):
60 | if not method.exchange.startswith(self.EXCHANGE_PREFIX_BROADCAST):
61 | return
62 |
63 | event_name = method.exchange[len(self.EXCHANGE_PREFIX_BROADCAST):]
64 | self.on_event_received(event_name, body)
65 |
66 | def on_event_received(self, event_name, event_body):
67 | handler = self._handlers.get(event_name)
68 | if not handler:
69 | return
70 | if isinstance(event_body, bytes):
71 | event_body = event_body.decode()
72 |
73 | event_body = json.loads(event_body)
74 | self.run_on_main_thread(handler, [event_body], {})
75 |
76 | def emit_event(self, event_name, event_body):
77 | exchange_name = self.EXCHANGE_PREFIX_BROADCAST + event_name
78 |
79 | self.channel.basic_publish(
80 | exchange=exchange_name,
81 | routing_key='',
82 | body=json.dumps(event_body)
83 | )
84 |
85 | def __del__(self):
86 | if hasattr(self, "channel"):
87 | self.channel.close()
88 |
--------------------------------------------------------------------------------
/gemstone/util.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import importlib
3 |
4 | from gemstone.client import RemoteService, AsyncMethodCall
5 |
6 |
7 | def init_default_logger():
8 | logging.basicConfig(
9 | level=logging.DEBUG,
10 | )
11 | return logging.getLogger()
12 |
13 |
14 | def as_completed(*async_result_wrappers):
15 | """
16 | Yields results as they become available from asynchronous method calls.
17 |
18 | Example usage
19 |
20 | ::
21 |
22 | async_calls = [service.call_method_async("do_stuff", (x,)) for x in range(25)]
23 |
24 | for async_call in gemstone.as_completed(*async_calls):
25 | print("just finished with result ", async_call.result())
26 |
27 | :param async_result_wrappers: :py:class:`gemstone.client.structs.AsyncMethodCall` instances.
28 | :return: a generator that yields items as soon they results become available.
29 |
30 | .. versionadded:: 0.5.0
31 | """
32 | for item in async_result_wrappers:
33 | if not isinstance(item, AsyncMethodCall):
34 | raise TypeError("Got non-AsyncMethodCall object: {}".format(item))
35 |
36 | wrappers_copy = list(async_result_wrappers)
37 |
38 | while len(wrappers_copy):
39 | completed = list(filter(lambda x: x.finished(), wrappers_copy))
40 | if not len(completed):
41 | continue
42 |
43 | for item in completed:
44 | wrappers_copy.remove(item)
45 | yield item
46 |
47 |
48 | def first_completed(*async_result_wrappers):
49 | """
50 | Just like :py:func:`as_completed`, but returns only the first item and discards the
51 | rest.
52 |
53 | :param async_result_wrappers:
54 | :return:
55 |
56 | .. versionadded:: 0.5.0
57 | """
58 | for item in async_result_wrappers:
59 | if not isinstance(item, AsyncMethodCall):
60 | raise TypeError("Got non-AsyncMethodCall object: {}".format(item))
61 | wrappers_copy = list(async_result_wrappers)
62 | while True:
63 | completed = list(filter(lambda x: x.finished(), wrappers_copy))
64 | if not len(completed):
65 | continue
66 |
67 | return completed[0].result()
68 |
69 |
70 | def get_remote_service_instance_for_url(url):
71 | return RemoteService(url)
72 |
73 |
74 | def dynamic_load(module_or_member):
75 | """
76 | Dynamically loads a class or member of a class.
77 |
78 | If ``module_or_member`` is something like ``"a.b.c"``, will perform ``from a.b import c``.
79 |
80 | If ``module_or_member`` is something like ``"a"`` will perform ``import a``
81 |
82 | :param module_or_member: the name of a module or member of a module to import.
83 | :return: the returned entity, be it a module or member of a module.
84 | """
85 | parts = module_or_member.split(".")
86 | if len(parts) > 1:
87 | name_to_import = parts[-1]
88 | module_to_import = ".".join(parts[:-1])
89 | else:
90 | name_to_import = None
91 | module_to_import = module_or_member
92 |
93 | module = importlib.import_module(module_to_import)
94 | if name_to_import:
95 | to_return = getattr(module, name_to_import)
96 | if not to_return:
97 | raise AttributeError("{} has no attribute {}".format(module, name_to_import))
98 | return to_return
99 | else:
100 | return module
101 |
--------------------------------------------------------------------------------
/gemstone/core/decorators.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import re
3 |
4 | import tornado.gen
5 |
6 | __all__ = [
7 | 'event_handler',
8 | 'exposed_method'
9 | ]
10 |
11 | METHOD_NAME_REGEX = re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]*$')
12 |
13 |
14 | def event_handler(event_name):
15 | """
16 | Decorator for designating a handler for an event type. ``event_name`` must be a string
17 | representing the name of the event type.
18 |
19 | The decorated function must accept a parameter: the body of the received event,
20 | which will be a Python object that can be encoded as a JSON (dict, list, str, int,
21 | bool, float or None)
22 |
23 | :param event_name: The name of the event that will be handled. Only one handler per
24 | event name is supported by the same microservice.
25 | """
26 |
27 | def wrapper(func):
28 | func._event_handler = True
29 | func._handled_event = event_name
30 | return func
31 |
32 | return wrapper
33 |
34 |
35 | def exposed_method(name=None, private=False, is_coroutine=True, requires_handler_reference=False):
36 | """
37 | Marks a method as exposed via JSON RPC.
38 |
39 | :param name: the name of the exposed method. Must contains only letters, digits, dots and underscores.
40 | If not present or is set explicitly to ``None``, this parameter will default to the name
41 | of the exposed method.
42 | If two methods with the same name are exposed, a ``ValueError`` is raised.
43 | :type name: str
44 | :param private: Flag that specifies if the exposed method is private.
45 | :type private: bool
46 | :param is_coroutine: Flag that specifies if the method is a Tornado coroutine. If True, it will be wrapped
47 | with the :py:func:`tornado.gen.coroutine` decorator.
48 | :type is_coroutine: bool
49 | :param requires_handler_reference: If ``True``, the handler method will receive as the first
50 | parameter a ``handler`` argument with the Tornado
51 | request handler for the current request. This request handler
52 | can be further used to extract various information from the
53 | request, such as headers, cookies, etc.
54 | :type requires_handler_reference: bool
55 |
56 | .. versionadded:: 0.9.0
57 |
58 | """
59 |
60 | def wrapper(func):
61 |
62 | # validation
63 |
64 | if name:
65 | method_name = name
66 | else:
67 | method_name = func.__name__
68 |
69 | if not METHOD_NAME_REGEX.match(method_name):
70 | raise ValueError("Invalid method name: '{}'".format(method_name))
71 |
72 | @functools.wraps(func)
73 | def real_wrapper(*args, **kwargs):
74 | return func(*args, **kwargs)
75 |
76 | # set appropriate flags
77 | if private:
78 | setattr(real_wrapper, "_exposed_private", True)
79 | else:
80 | setattr(real_wrapper, "_exposed_public", True)
81 |
82 | if is_coroutine:
83 | real_wrapper.__gemstone_is_coroutine = True
84 | real_wrapper = tornado.gen.coroutine(real_wrapper)
85 | setattr(real_wrapper, "_is_coroutine", True)
86 |
87 | if requires_handler_reference:
88 | setattr(real_wrapper, "_req_h_ref", True)
89 |
90 | setattr(real_wrapper, "_exposed_name", method_name)
91 |
92 | return real_wrapper
93 |
94 | return wrapper
95 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. pymicroservice documentation master file, created by
2 | sphinx-quickstart on Fri Nov 25 14:52:22 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to gemstone's documentation!
7 | ====================================
8 |
9 |
10 | The **gemstone** library aims to provide an easy way to develop simple and
11 | scalable microservices by using the asynchronous features of Python.
12 |
13 | This library offers support for writing a microservice that:
14 |
15 | - exposes a public Json RPC 2.0 HTTP API
16 | (see `The JSON RPC 2.0 specifications