From 64427aba50c6589b632b87ddaa995562778d249a Mon Sep 17 00:00:00 2001 From: BryantHe Date: Wed, 19 Jul 2023 19:06:53 +0800 Subject: [PATCH] add commad line tool --- .gitignore | 4 +- MANIFEST.in | 3 + README.md | 22 +++++- namekoplus/__init__.py | 1 + namekoplus/chassis/__init__.py | 0 namekoplus/chassis/chassis.py | 20 ++++++ namekoplus/command.py | 85 +++++++++++++++++++++++ namekoplus/templates/__init__.py | 0 namekoplus/templates/event/__init__.py | 0 namekoplus/templates/event/config.yml | 24 +++++++ namekoplus/templates/event/events_demo.py | 60 ++++++++++++++++ namekoplus/templates/http/__init__.py | 0 namekoplus/templates/http/config.yml | 24 +++++++ namekoplus/templates/http/http_demo.py | 32 +++++++++ namekoplus/templates/rpc/__init__.py | 0 namekoplus/templates/rpc/config.yml | 24 +++++++ namekoplus/templates/rpc/rpc_demo.py | 31 +++++++++ namekoplus/templates/timer/__init__.py | 0 namekoplus/templates/timer/config.yml | 24 +++++++ namekoplus/templates/timer/timer_demo.py | 17 +++++ setup.cfg | 0 setup.py | 71 +++++++++++++++++++ 22 files changed, 439 insertions(+), 3 deletions(-) create mode 100644 MANIFEST.in create mode 100644 namekoplus/__init__.py create mode 100644 namekoplus/chassis/__init__.py create mode 100644 namekoplus/chassis/chassis.py create mode 100644 namekoplus/command.py create mode 100644 namekoplus/templates/__init__.py create mode 100644 namekoplus/templates/event/__init__.py create mode 100644 namekoplus/templates/event/config.yml create mode 100644 namekoplus/templates/event/events_demo.py create mode 100644 namekoplus/templates/http/__init__.py create mode 100644 namekoplus/templates/http/config.yml create mode 100644 namekoplus/templates/http/http_demo.py create mode 100644 namekoplus/templates/rpc/__init__.py create mode 100644 namekoplus/templates/rpc/config.yml create mode 100644 namekoplus/templates/rpc/rpc_demo.py create mode 100644 namekoplus/templates/timer/__init__.py create mode 100644 namekoplus/templates/timer/config.yml create mode 100644 namekoplus/templates/timer/timer_demo.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 68bc17f..1eedc47 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,6 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +*.pypirc \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..05c5da4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include *.py +recursive-include namekoplus/chassis *.py +recursive-include namekoplus/templates *.py *.yml \ No newline at end of file diff --git a/README.md b/README.md index 254f361..4f1f91c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# nameko-plus -A Python distributed microservice solution +# namekoplus +A lightweight Python distributed microservice solution + +## Document + +[中文文档](https://doc.bearcatlog.com/) + +## Command Line Tool Usage + +### Checkout Command + +```shell +namekoplus --help +``` + +### Initialize a nameko service from templates + +```shell +namekoplus init --directory --type +``` diff --git a/namekoplus/__init__.py b/namekoplus/__init__.py new file mode 100644 index 0000000..47f7003 --- /dev/null +++ b/namekoplus/__init__.py @@ -0,0 +1 @@ +from namekoplus.chassis.chassis import * \ No newline at end of file diff --git a/namekoplus/chassis/__init__.py b/namekoplus/chassis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/namekoplus/chassis/chassis.py b/namekoplus/chassis/chassis.py new file mode 100644 index 0000000..7fdcc51 --- /dev/null +++ b/namekoplus/chassis/chassis.py @@ -0,0 +1,20 @@ +def init_statsd(prefix=None, host=None, port=8125): + from statsd import StatsClient + statsd = StatsClient(host, port, prefix=prefix) + return statsd + + +def init_logger(): + import logging + from logstash_formatter import LogstashFormatterV1 + logger = logging.getLogger() + handler = logging.StreamHandler() + formatter = LogstashFormatterV1() + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +def init_sentry(): + from nameko_sentry import SentryReporter + return SentryReporter() diff --git a/namekoplus/command.py b/namekoplus/command.py new file mode 100644 index 0000000..694e379 --- /dev/null +++ b/namekoplus/command.py @@ -0,0 +1,85 @@ +import os +import shutil +from contextlib import contextmanager + +import click + + +@contextmanager +def status(status_msg: str, newline: bool = False, quiet: bool = False): + msg_suffix = ' ...' if not newline else ' ...\n' + click.echo(status_msg + msg_suffix) + try: + yield + except Exception as e: + if not quiet: + click.echo(' FAILED\n') + raise + else: + if not quiet: + click.echo(' Done\n') + + +def get_template_directory() -> str: + """Return the directory where nameko_plus setup templates are found. + + This method is used by the nameko_plus ``init`` commands. + """ + import namekoplus + + package_dir = os.path.abspath(os.path.dirname(namekoplus.__file__)) + return os.path.join(package_dir, 'templates') + + +@click.group() +def cli(): + pass + + +@cli.command() +@click.option('-d', '--directory', + required=True, + help='The directory name of nameko services') +@click.option('-f', '--type', '_type', + default='rpc', + show_default=True, + type=click.Choice(['rpc', 'event', 'http', 'timer'], case_sensitive=False), + help='The template type of nameko service') +def init(directory, _type): + """ + Initialize a new service via templates. + """ + if os.access(directory, os.F_OK) and os.listdir(directory): + click.echo('Directory {} already exists and is not empty'.format(directory), err=True) + return + + template_dir = os.path.join(get_template_directory(), _type) + if not os.access(template_dir, os.F_OK): + click.echo('No such template type {}'.format(_type), err=True) + return + + # 创建目录 + if not os.access(directory, os.F_OK): + with status(f'Creating directory {os.path.abspath(directory)!r}'): + os.makedirs(directory) + + # 把 templates 放入新建的目录 + for file_ in os.listdir(template_dir): + if file_ == '__pycache__': + continue + src_file_path = os.path.join(template_dir, file_) + output_file = os.path.join(directory, file_) + with status(f'Generating {os.path.abspath(output_file)}'): + shutil.copy(src_file_path, output_file) + + +@cli.command() +def start(): + """ + Start a middleware, such as RabbitMQ. + """ + click.echo('Initialized the database') + + +if __name__ == '__main__': + cli() \ No newline at end of file diff --git a/namekoplus/templates/__init__.py b/namekoplus/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/namekoplus/templates/event/__init__.py b/namekoplus/templates/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/namekoplus/templates/event/config.yml b/namekoplus/templates/event/config.yml new file mode 100644 index 0000000..f8f8f3e --- /dev/null +++ b/namekoplus/templates/event/config.yml @@ -0,0 +1,24 @@ +AMQP_URI: pyamqp://${RABBIT_USER:guest}:${RABBIT_PASSWORD:guest}@${RABBIT_HOST:localhost}:${RABBIT_PORT:5672}/ +RPC_EXCHANGE: 'nameko-rpc' + +max_workers: 10 +parent_calls_tracked: 20 + +LOGGING: + version: 1 + formatters: + tracer: + (): nameko_tracer.formatters.PrettyJSONFormatter + handlers: + tracer: + class: logging.StreamHandler + formatter: tracer + loggers: + nameko_tracer: + level: INFO + handlers: [tracer] + +SENTRY: + DSN: ${SENTRY_DSN} + CLIENT_CONFIG: + site: ${SENTRY_SITE} \ No newline at end of file diff --git a/namekoplus/templates/event/events_demo.py b/namekoplus/templates/event/events_demo.py new file mode 100644 index 0000000..1221ce5 --- /dev/null +++ b/namekoplus/templates/event/events_demo.py @@ -0,0 +1,60 @@ +from nameko.events import EventDispatcher, event_handler +from nameko.rpc import rpc +from nameko_tracer import Tracer +from namekoplus import init_statsd, init_sentry + + +class EventPublisherService: + name = "publisher_service" + + dispatch = EventDispatcher() + + tracer = Tracer() + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @rpc + @statsd.timer('publish') + def publish(self, event_type, payload): + self.dispatch(event_type, payload) + + +class AnEventListenerService: + name = "an_event_listener_service" + + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @event_handler("publisher_service", "an_event") + @statsd.timer('consume_an_event') + def consume_an_event(self, payload): + print("service {} received:".format(self.name), payload) + + +class AnotherEventListenerService: + name = "another_event_listener_service" + + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @event_handler("publisher_service", "another_event") + @statsd.timer('consume_another_event') + def consume_another_event(self, payload): + print("service {} received:".format(self.name), payload) + + +class ListenBothEventsService: + name = "listen_both_events_service" + + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @event_handler("publisher_service", "an_event") + @statsd.timer('consume_an_event') + def consume_an_event(self, payload): + print("service {} received:".format(self.name), payload) + + @event_handler("publisher_service", "another_event") + @statsd.timer('consume_another_event') + def consume_another_event(self, payload): + print("service {} received:".format(self.name), payload) diff --git a/namekoplus/templates/http/__init__.py b/namekoplus/templates/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/namekoplus/templates/http/config.yml b/namekoplus/templates/http/config.yml new file mode 100644 index 0000000..f8f8f3e --- /dev/null +++ b/namekoplus/templates/http/config.yml @@ -0,0 +1,24 @@ +AMQP_URI: pyamqp://${RABBIT_USER:guest}:${RABBIT_PASSWORD:guest}@${RABBIT_HOST:localhost}:${RABBIT_PORT:5672}/ +RPC_EXCHANGE: 'nameko-rpc' + +max_workers: 10 +parent_calls_tracked: 20 + +LOGGING: + version: 1 + formatters: + tracer: + (): nameko_tracer.formatters.PrettyJSONFormatter + handlers: + tracer: + class: logging.StreamHandler + formatter: tracer + loggers: + nameko_tracer: + level: INFO + handlers: [tracer] + +SENTRY: + DSN: ${SENTRY_DSN} + CLIENT_CONFIG: + site: ${SENTRY_SITE} \ No newline at end of file diff --git a/namekoplus/templates/http/http_demo.py b/namekoplus/templates/http/http_demo.py new file mode 100644 index 0000000..bac7e62 --- /dev/null +++ b/namekoplus/templates/http/http_demo.py @@ -0,0 +1,32 @@ +import json +from nameko.web.handlers import http +from nameko_tracer import Tracer +from werkzeug.wrappers import Response +from namekoplus import init_statsd, init_sentry + + +class HttpDemoService: + name = "http_demo_service" + + tracer = Tracer() + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @http("GET", "/broken") + @statsd.timer('broken') + def broken(self, request): + raise ConnectionRefusedError() + + @http('GET', '/books/') + @statsd.timer('demo_get') + def demo_get(self, request, uuid): + data = {'id': uuid, 'title': 'The unbearable lightness of being', + 'author': 'Milan Kundera'} + return Response(json.dumps({'book': data}), + mimetype='application/json') + + @http('POST', '/books') + @statsd.timer('demo_post') + def demo_post(self, request): + return Response(json.dumps({'book': request.data.decode()}), + mimetype='application/json') diff --git a/namekoplus/templates/rpc/__init__.py b/namekoplus/templates/rpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/namekoplus/templates/rpc/config.yml b/namekoplus/templates/rpc/config.yml new file mode 100644 index 0000000..f8f8f3e --- /dev/null +++ b/namekoplus/templates/rpc/config.yml @@ -0,0 +1,24 @@ +AMQP_URI: pyamqp://${RABBIT_USER:guest}:${RABBIT_PASSWORD:guest}@${RABBIT_HOST:localhost}:${RABBIT_PORT:5672}/ +RPC_EXCHANGE: 'nameko-rpc' + +max_workers: 10 +parent_calls_tracked: 20 + +LOGGING: + version: 1 + formatters: + tracer: + (): nameko_tracer.formatters.PrettyJSONFormatter + handlers: + tracer: + class: logging.StreamHandler + formatter: tracer + loggers: + nameko_tracer: + level: INFO + handlers: [tracer] + +SENTRY: + DSN: ${SENTRY_DSN} + CLIENT_CONFIG: + site: ${SENTRY_SITE} \ No newline at end of file diff --git a/namekoplus/templates/rpc/rpc_demo.py b/namekoplus/templates/rpc/rpc_demo.py new file mode 100644 index 0000000..ab2311b --- /dev/null +++ b/namekoplus/templates/rpc/rpc_demo.py @@ -0,0 +1,31 @@ +from nameko.rpc import rpc, ServiceRpc +from nameko_tracer import Tracer +from namekoplus import init_statsd, init_sentry + + +class RpcResponderDemoService: + name = "rpc_responder_demo_service" + + tracer = Tracer() + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @rpc + @statsd.timer('hello') + def hello(self, name): + return "Hello, {}!".format(name) + + +class RpcCallerDemoService: + name = "rpc_caller_demo_service" + + remote = ServiceRpc("rpc_responder_demo_service") + + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @rpc + @statsd.timer('remote_hello') + def remote_hello(self, value="John Doe"): + res = u"{}".format(value) + return self.remote.hello(res) diff --git a/namekoplus/templates/timer/__init__.py b/namekoplus/templates/timer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/namekoplus/templates/timer/config.yml b/namekoplus/templates/timer/config.yml new file mode 100644 index 0000000..f8f8f3e --- /dev/null +++ b/namekoplus/templates/timer/config.yml @@ -0,0 +1,24 @@ +AMQP_URI: pyamqp://${RABBIT_USER:guest}:${RABBIT_PASSWORD:guest}@${RABBIT_HOST:localhost}:${RABBIT_PORT:5672}/ +RPC_EXCHANGE: 'nameko-rpc' + +max_workers: 10 +parent_calls_tracked: 20 + +LOGGING: + version: 1 + formatters: + tracer: + (): nameko_tracer.formatters.PrettyJSONFormatter + handlers: + tracer: + class: logging.StreamHandler + formatter: tracer + loggers: + nameko_tracer: + level: INFO + handlers: [tracer] + +SENTRY: + DSN: ${SENTRY_DSN} + CLIENT_CONFIG: + site: ${SENTRY_SITE} \ No newline at end of file diff --git a/namekoplus/templates/timer/timer_demo.py b/namekoplus/templates/timer/timer_demo.py new file mode 100644 index 0000000..365c3d5 --- /dev/null +++ b/namekoplus/templates/timer/timer_demo.py @@ -0,0 +1,17 @@ +from nameko.timer import timer +from nameko_tracer import Tracer +from namekoplus import init_statsd, init_sentry + + +class Service: + name = "service" + + tracer = Tracer() + sentry = init_sentry() + statsd = init_statsd('statsd_prefix', 'statsd_host', 'statsd_port') + + @timer(interval=1) + @statsd.timer('ping') + def ping(self): + # method executed every second + print("pong") \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ff14fce --- /dev/null +++ b/setup.py @@ -0,0 +1,71 @@ +from setuptools import setup, find_packages +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='namekoplus', + version='0.1.0', + description='A lightweight Python distributed microservice solution', + long_description=long_description, + url='https://github.com/Bryanthelol/namekoplus', + + author='Bryant He', + author_email='bryantsisu@qq.com', + + license='MIT', + + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + platforms='any', + python_requires='>=3', + + keywords='lightweight python distributed microservice solution', + + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + include_package_data=True, + + entry_points={ + 'console_scripts': [ + 'namekoplus = namekoplus.command:cli', + ], + }, + + install_requires=[ + 'nameko==3.0.0rc11', + 'nameko-sentry==1.0.0', + 'nameko-tracer==1.4.0', + 'click==8.1.5', + 'pytest==7.4.0', + 'environs==9.5.0', + 'logstash_formatter==0.5.17', + 'statsd==4.0.1', + 'tenacity==8.2.2', + 'cachetools==5.3.0', + 'circuitbreaker==2.0.0', + 'shortuuid==1.0.11', + 'cryptography' + ], + extras_require={ + 'apiflask': ['apiflask>=1.3.1', + 'gevent>=22.10.2', + 'gunicorn==20.1.0'], + 'rocketry': ['rocketry==2.4.0'], + 'gutter': ['gutter==0.5.0'], + 'mysql': ['pymysql==1.0.3', + 'sqlalchemy==2.0.15', + 'sqlacodegen==2.3.0', + 'alembic==1.11.1'], + 'dev': ['mako==1.2.4'], + }, +)