├── .env ├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── docs ├── framework.md ├── helloworld.md ├── libraries.md ├── log.md ├── manage.md └── settings.md ├── requirements.txt ├── servos ├── __init__.py ├── contrib │ ├── __init__.py │ └── i18n │ │ ├── __init__.py │ │ ├── middle_i18n.py │ │ └── settings.ini ├── core │ ├── __init__.py │ ├── commands.py │ ├── default_settings.ini │ ├── dispatch.py │ ├── service.py │ └── simpleframe.py ├── i18n │ ├── __init__.py │ ├── trans_null.py │ └── trans_real.py ├── manage.py ├── template_files │ ├── command │ │ └── commands.py │ ├── project │ │ ├── .gitignore.template │ │ ├── manage.py │ │ ├── services │ │ │ ├── __init__.py │ │ │ ├── local_settings.ini │ │ │ └── settings.ini │ │ └── setup.py │ └── service │ │ ├── __init__.py │ │ ├── requirements.txt │ │ ├── service.py │ │ └── settings.ini └── utils │ ├── __init__.py │ ├── common.py │ ├── localproxy.py │ └── pyini.py ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.env: -------------------------------------------------------------------------------- 1 | VERSION=100.1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude static fiels generated by 'python manage.py collectstatic --noinput' 2 | /static/ 3 | 4 | # Generated by `python manage.py compilemessages` translation files 5 | *.mo 6 | # temporary django makemessages file 7 | *.pot 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Generated by `python setup.py sdist` 17 | AUTHORS 18 | ChangeLog 19 | 20 | # Distribution / packaging 21 | .Python 22 | env/ 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | local/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | *.eggs 37 | # ssh key file 38 | id_rsa 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .cache 55 | .venv 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Pycharm settings 61 | .idea 62 | 63 | # linux editor temp file 64 | *~ 65 | 66 | # Eclipse settings 67 | *.project 68 | *.pydevproject 69 | 70 | # vi/vim temp file 71 | *.swp 72 | 73 | # sqlite database 74 | *.sqlite3 75 | 76 | # Windows image file caches 77 | Thumbs.db 78 | ehthumbs.db 79 | 80 | # Windows folder config file 81 | Desktop.ini 82 | 83 | # MAC OSX 84 | .DS_Store 85 | .AppleDouble 86 | .LSOverride 87 | 88 | *.log 89 | .*.md.html 90 | .virtualenvs 91 | .settings 92 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | 4 | build_release_job: 5 | stage: build 6 | only: 7 | - tags 8 | script: 9 | - pip install -r test-requirements.txt 10 | - python setup.py bdist_wheel upload -r local 11 | tags: 12 | - portal-tests 13 | 14 | build_special_job: 15 | stage: build 16 | only: 17 | - develop 18 | - staging 19 | - /^if\/.*$/ 20 | script: 21 | - source ./.env 22 | - pip install -r test-requirements.txt 23 | - sed -i "N;2a\version = ${VERSION}" ./setup.cfg 24 | - python setup.py bdist_wheel upload -r local 25 | tags: 26 | - portal-tests 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Servos Framework: *The process level Concurrent Toolkit* 3 | [![Python 2.7](https://img.shields.io/badge/python-2.7-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/Xyntax/POC-T/master/doc/LICENSE.txt) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1413552d34bc4a4aa84539db1780eb56)](https://www.codacy.com/app/xyntax/POC-T?utm_source=github.com&utm_medium=referral&utm_content=Xyntax/POC-T&utm_campaign=Badge_Grade) 4 | 5 | ## 框架说明 6 | 7 | ### 服务的组织 8 | 采用分散开发统一管理。以服务为单位进行开发。每个服务可以单独启动,也可以多个服务同时启动,通过配置文件进行管理指定哪些服务生效,部署方式灵活。 9 | 10 | 特点 11 | --- 12 | * 框架代码结构简单易用,易于修改。新手、老鸟皆可把控。 13 | 14 | * 采用gevent实现并发操作,与scrapy的twisted相比,代码更容易理解。 15 | 16 | * 完全模块化的设计,强大的可扩展性。 17 | 18 | * 支持多线程/Gevent两种并发模式 19 | 20 | * 支持分布式 21 | 22 | ### 扩展性 23 | - plugin扩展。框架已经预设了一些调用点,方便对各个环节进行修改。可以针对这些调用点编写相应的处理,当框架启动时会自动进行采集,当程序运行到调用点位置时,自动调用对应的插件函数。 24 | - middleware 中间件扩展。与`web framework`类似,这里是对服务接口调用进行扩展。 25 | - injection 注入式扩展。可向框架注入 functions, global_objects 等。 26 | 27 | ## 新增服务流程 28 | - 增加服务包。 29 | - 从基类继承,并使用 `entry` 装饰器标记。 30 | - 编码服务逻辑。 31 | - 配置文件中增加要启动的服务。 32 | 33 | 用户手册 34 | ---- 35 | 36 | * [快速开始](https://github.com/knitmesh/servos-framework/blob/master/docs/helloworld.md) 37 | * [日志处理说明](https://github.com/knitmesh/servos-framework/blob/master/docs/log.md) 38 | * [命令行命令说明](https://github.com/knitmesh/servos-framework/blob/master/docs/manage.md) 39 | * [配置文件说明](https://github.com/knitmesh/servos-framework/blob/master/docs/settings.md) 40 | 41 | 依赖 42 | --- 43 | * Python 2.7 44 | * pip 45 | 46 | 47 | ### 安装 48 | 49 | - 安装服务框架。新构建的服务依赖 **servos-framework** 框架,先 ```pip install servos-framework```。 50 | - 安装服务依赖。运行命令 ```servos install```,该命令会自动安装各服务在自己目录下 requirements.txt 定义的依赖包,具体参数可以运行 ```servos help``` 命令查看。 51 | - 启动服务。运行 ```servos runserver --settings cache_settings.ini```。 52 | - 调试服务。使用 **manage.py** 作为启动文件,在IDE的 Parameters 配置处输入将要使用的命令行, 如: ```runserver --settings cache_settings.ini```。 53 | 54 | ### 部署安装 55 | 56 | - 执行`./setenv.sh`会自动创建virtualenv虚拟环境。 57 | - 执行`source .virtualenvs/services/bin/activate`切换到虚拟环境。 58 | - 启动服务。 59 | 1. cache服务:运行 ```servos runserver --settings cache_settings.ini```。 60 | 2. websocket服务:运行 ```servos runserver --settings wss_settings.ini```。 61 | 62 | ### Docker 63 | 构建base镜像 64 | ```bash 65 | # 初始化外部依赖 66 | python2 docker/prepare.py -v VERSION --pip-server 192.168.103.137:8000 --yum-repo http://192.168.103.137:8001 67 | docker build . -f docker/base/Dockerfile -t service_base:VERSION 68 | ``` 69 | 70 | 构建功能镜像(以cache为例) 71 | ```bash 72 | cd docker/cache/ 73 | docker build . -t service-cache:VERSION 74 | ``` 75 | 76 | 运行服务(以cache为例) 77 | ```bash 78 | docker run --rm --network host \ 79 | -v /etc/portal/cache_settings.ini:/etc/portal/cache_settings.ini \ 80 | --name service-wss \ 81 | t2cloud-service-wss:VERSION 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/framework.md: -------------------------------------------------------------------------------- 1 | ## 框架说明 2 | 3 | ### 服务的组织 4 | 采用分散开发统一管理。以服务为单位进行开发。每个服务可以单独启动,也可以多个服务同时启动,通过配置文件进行管理指定哪些服务生效,部署方式灵活。 5 | 6 | ### 扩展性 7 | - plugin扩展。框架已经预设了一些调用点,方便对各个环节进行修改。可以针对这些调用点编写相应的处理,当框架启动时会自动进行采集,当程序运行到调用点位置时,自动调用对应的插件函数。 8 | - middleware 中间件扩展。与`web framework`类似,这里是对服务接口调用进行扩展。 9 | - injection 注入式扩展。可向框架注入 functions, global_objects 等。 10 | 11 | ### 低耦合 12 | 服务间降低耦合,只依赖框架。 13 | 14 | ### 配置信息管理 15 | settings对象。 16 | 17 | ### 服务管理 18 | dispatcher对象 19 | 20 | ## 新增服务流程 21 | - 增加服务包。 22 | - 从基类继承,并使用 `entry` 装饰器标记。 23 | - 编码服务逻辑。 24 | - 配置文件中增加要启动的服务。 25 | 26 | -------------------------------------------------------------------------------- /docs/helloworld.md: -------------------------------------------------------------------------------- 1 | # Hello, World 2 | 3 | ## 准备 4 | 5 | 安装python-servos 6 | 7 | 8 | ## 创建新的项目 9 | 10 | 在安装完毕后,会提供一个命令行工具 servos, 它可以执行一些命令。 11 | 12 | 进入你的工作目录,然后执行: 13 | 14 | 15 | ``` 16 | servos makeproject project-demo 17 | ``` 18 | 19 | 执行成功后,在 project-demo 目录下会是这样的: 20 | 21 | 22 | ``` 23 | ├── services 24 | │   ├── __init__.py 25 | │   ├── local_settings.ini 26 | │   └── settings.ini 27 | ├── manage.py 28 | └── setup.py 29 | ``` 30 | 31 | 32 | ## 创建Hello服务 33 | 34 | 然后让我们创建一个Hello的服务: 35 | 36 | 37 | ``` 38 | cd project-demo 39 | servos makeservice Hello 40 | ``` 41 | 42 | 在执行成功后,你会在services/Hello下看到: 43 | 44 | 45 | ``` 46 | ├── __init__.py 47 | ├── requirements.txt 48 | ├── service.py 49 | └── settings.ini 50 | ``` 51 | 52 | 53 | ## 编写服务代码 54 | 55 | 打开 Hello/service.py,你会看到: 56 | 57 | 58 | ```python 59 | # -*- coding: utf-8 -*- 60 | from servos import ServiceBase, entry, settings 61 | logger = logging.getLogger(__name__) 62 | 63 | 64 | @entry() 65 | class DemoService(ServiceBase): 66 | _service_name = 'demoservice' 67 | 68 | def start(self): 69 | logger.info('starting %s service' % self._service_name) 70 | super(DemoService, self).start() 71 | self.tg.add_thread(self._do_loop, 'demo_thread') 72 | logger.info('%s service started' % self._service_name) 73 | 74 | def _do_loop(self, name, *args, **kwargs): 75 | while True: 76 | logger.info('%s print in thread loop', name) 77 | threading.time.sleep(10) 78 | logger.info('%s exit loop', name) 79 | 80 | def stop(self): 81 | logger.info('stoping %s service' % self._service_name) 82 | super(DemoService, self).stop() 83 | logger.info('%s service stoped' % self._service_name) 84 | 85 | ``` 86 | 87 | 该demo服务会使用线程池中的线程循环打印信息。 88 | 89 | 90 | 91 | ## 启动 92 | 93 | 在命令行下执行: 94 | 95 | 96 | ``` 97 | servos runserver --settings settings.ini 98 | ``` 99 | 100 | 这样就启动了所有服务。 101 | 102 | 也可以使用 `manage.py` 作为启动文件来进行调试,在IDE的 Parameters 配置处输入将要使用的命令行, 如: 103 | 104 | ``` 105 | runserver --settings settings.ini 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/libraries.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knitmesh/servos-framework/45fdc04580890c3aac039c023f104ce8dc00af08/docs/libraries.md -------------------------------------------------------------------------------- /docs/log.md: -------------------------------------------------------------------------------- 1 | # 日志处理说明 2 | 3 | ## 日志配置 4 | 5 | 目前框架在default_settings.ini中已经有如下配置: 6 | 7 | 8 | ``` 9 | [LOG] 10 | #level, filename, filemode, datefmt, format can be used in logging.basicConfig 11 | level = 'info' 12 | format = "[%(levelname)s %(name)s %(asctime)-15s %(filename)s,%(lineno)d] %(message)s" 13 | 14 | [LOG.Loggers] 15 | ROOT={'handlers':['Full']} 16 | 17 | [LOG.Handlers] 18 | Full = {'format':'format_full','class':'logging.handlers.RotatingFileHandler','args':('default.log','w',10000000,15)} 19 | Simple = {'format':'format_simple'} 20 | Package = {'format':'format_package'} 21 | 22 | #defines all log fomatters 23 | [LOG.Formatters] 24 | format_full = "[%(levelname)s %(name)s %(asctime)-15s %(filename)s,%(lineno)d] %(message)s" 25 | format_simple = "[%(levelname)s] %(message)s" 26 | format_package = "[%(levelname)s %(name)s] %(message)s" 27 | ``` 28 | 29 | 日志可以分为:全局配置、root logger配置、其它logger配置。同时为了配置上可以复用, 30 | handler和formatter可以单独配置,这样在logger中配置时只要引用相当的名字就可以了。 31 | 32 | ### basicConfig 33 | 34 | [LOG]中定义的是全局配置,它对应于使用logging.basicConfig()的方式。因此你可以在[LOG]中定义如下参数: 35 | 36 | 37 | level -- 38 | 日志级别,如:'info', 'debug', 'error', 'warn', 'notset'('noset'表示未设置)。缺省是NOTSET。 39 | 40 | filename -- 41 | 文件名。当你需要将日志记录到文件中时使用。缺省是使用标准输出。 42 | 43 | filemode -- 44 | 写入文件时的模式。需要先设置filename。缺省为'a'(追加方式),可以设置为'w'(表 45 | 示写入方式,会覆盖原日志文件)。 46 | 47 | datefmt -- 48 | 日期格式,按datetime格式串的要求进行配置,缺省为'yyyy-mm-dd hh:mm:ss,ms'。 49 | 50 | format -- 51 | 日志输出格式串。详见 [Python的日志记录属性](http://docs.python.org/library/logging.html#logrecord-attributes) 。 52 | 53 | 54 | 55 | 从logging中定义的日志级别可以看到有:CRITICAL(同FATAL), ERROR, WARNING(同WARN), 56 | INFO, DEBUG, NOTSET。级别的大小是从高向低排的,最高的数值越大。当你设置了某个日志级别, 57 | 只有大于等于这个级别的才可以输出。一般来说,在创建logger或handler时,如果没有指定日志级别, 58 | 缺省都是NOTSET,所以所有的日志都会输出。 59 | 60 | 61 | ### logger定义 62 | 63 | 针对不同的logger,可以定义不同的日志配置。所有logger都定义在 `[LOG.Loggers]` 中。 64 | 它可以定义logger的level, 还可以定义多个handler,在缺省情况下,当不定义handler时, 65 | 会使用logging.StreamHandler类来处理。每个logger的配置形如: 66 | 67 | ``` 68 | [LOG.Loggers] 69 | key = value 70 | ``` 71 | 72 | 其中key就是logger的名字,如'servos.console', 'servos.core'等。 73 | 如果logger的名字为 'ROOT',则表示root logger。而root logger就是执行basicConfig()后的日志对象。 74 | 75 | value是一个字典,可以使用的参数说明为: 76 | 77 | - propagate -- 78 | 传播标志。缺省为1,如果不传播,则要设置为0。主要是因为logging中的日志是可以分级的, 79 | 在存在分级的情况下,当前logger处理完一条日志后,如果传播标志为1,则一旦存在父日志对象, 80 | 则会自动调用父日志handler来输出日志。因此,你要根据你的实际配置来决定要如何设置传播。 81 | 否则有可能出现一条日志会被输出多次的情况。 82 | 83 | - level -- 84 | logger的日志级别。 85 | 86 | - handlers -- 87 | 处理句柄,它是一个list。它与另一个参数'format'不能同时使用。而这里处理句柄只是一个名字的引用, 88 | 如:['handler1', 'handler2']。真正的处理句柄将在[LOG.Handlers]中定义。 89 | 90 | - format -- 91 | 日志输出格式串。不能与handlers连用。如果同时定义了handlers,则此项不生效。 92 | 当定义了format时,由handlers中会自动创建一个缺省的StreamHandler的句柄, 93 | 其格式串为format的值。这里格式串有两种处理方式,一种是它定义为后面[LOG.Formatters] 94 | 的一个名字的引用。另一种就是当在[LOG.Formatters]找不到时,则认为是一个普通的格式串进行处理。 95 | 96 | 97 | > 因为一个logger可以支持多个handler,而format只定义了某个handler的格式串。 98 | 在通常情况下,在定义handler时,同时可以定义它的日志级别和format信息。 99 | 所以format的参数在这里,只是用来处理最简单的情况。 100 | 101 | 102 | ### handler的定义 103 | 104 | 所有的handler都定义在 [LOG.Handlers] 中,形式为: 105 | 106 | ``` 107 | [LOG.Handlers] 108 | key = value 109 | ``` 110 | 111 | 其中key为handler的名字。 112 | 113 | value为handler要使用的参数,可以为: 114 | 115 | - class -- 116 | handler所对应的类对象。缺省为 `logging.StreamHandler` 。注意,这里加上了模块的路径,以便可以方便导入。 117 | 118 | - args -- 119 | 需要传入handler类进行初始化的位置参数。 120 | 121 | - kwargs -- 122 | 需要传入handler类进行初始化的名字参数。 123 | 124 | - level -- 125 | handler的日志级别。缺省为NOTSET。 126 | 127 | - format -- 128 | 当前handler使用的日志输出格式。它有两种定义方式,一种是和后面的[LOG.Formatters] 129 | 中的formatter对应,只是一个名字。另一种是当找不到一个名字时,会自动认为是格式串。 130 | 所以简单情况下,可以直接在handler中定义format串,而不是先在[LOG.Formatters] 131 | 中先定义好formatter,然后再引用它的名字。 132 | 133 | 134 | 从上面可以看出,handler和logger都可以定义自已的日志级别。同时root logger用于 135 | 定义缺省的日志级别。所你你可以根据需要在不同的对象上实现有区别的日志级别定义。 136 | 137 | 138 | ### formatter的定义 139 | 140 | 从前面可以看出,在定义logger和handler时都可以直接定义format串,并不一定需要定义 141 | formatter。那么formatter的存在只是为了复用。你可以先定义几种常用的日志格式, 142 | 然后在定义logger和handler时引用它,这样会比较简单。只不过要注意,在[LOG]中定义的format 143 | 不能是formatter的引用,因为它是要使用basicConfig()来处理的,而它是不接受一个 144 | Formatter对象的。formatter的定义形式为: 145 | 146 | ``` 147 | key = value 148 | ``` 149 | 150 | 共中key为formatter的名字。 151 | 152 | value为日志的格式串。具体定义参见 [Python的日志记录属性](http://docs.python.org/library/logging.html#logrecord-attributes) 。 153 | 154 | 155 | ## 应用介绍 156 | 157 | ### 使用 158 | 159 | 使用简单的日志可以直接使用root logger,方法为: 160 | 161 | ``` 162 | import logging 163 | logging.info() 164 | ``` 165 | 166 | 可以直接调用logging模块提供的相关的api进行输出。这是最简单的情况。 167 | 也可以主动获得root logger对象,如: 168 | 169 | 170 | ``` 171 | import logging 172 | logger = logging.getLogger('') 173 | ``` 174 | 175 | 使用某个命名logger对象,如: 176 | 177 | 178 | ``` 179 | import logging 180 | logger = logging.getLogger('cache_manager.cache_helper') 181 | ``` 182 | 183 | 如果,指定的日志名已经在settings.ini中配置了,则可以直接使用它的配置项。如果没有配置,则全部使用缺省的。 184 | 185 | 如果你使用了其它的组件,它们需要对日志进行配置,也可以在settings.ini中设置,一样可以生效。 186 | 187 | 188 | ### 使用建议 189 | 190 | 建议在你的程序中,每次要用到logger对象时,使用logging.getLogger(name)来获得一个logger对象。 191 | 192 | -------------------------------------------------------------------------------- /docs/manage.md: -------------------------------------------------------------------------------- 1 | # 命令行命令说明 2 | 3 | ## servos 4 | 5 | 当运行不带参数的servos命令时,会显示一个帮助信息,但是因为命令很多, 6 | 所以这个帮助只是列出可用命令的清单,如: 7 | 8 | 9 | ``` 10 | Usage: servos [global_options] [subcommand [options] [args]] 11 | 12 | Global Options: 13 | --help show this help message and exit. 14 | -v, --verbose Output the result in verbose mode. 15 | -s SETTINGS, --settings=SETTINGS 16 | Settings file name. Default is "settings.ini". 17 | -y, --yes Automatic yes to prompt. 18 | -L LOCAL_SETTINGS, --local_settings=LOCAL_SETTINGS 19 | Local settings file name. Default is 20 | "local_settings.ini". 21 | -d SERVICES_DIR, --dir=SERVICES_DIR 22 | Your services directory, default is "services". 23 | --pythonpath=PYTHONPATH 24 | A directory to add to the Python path, e.g. 25 | "/home/myproject". 26 | --version show program's version number and exit. 27 | 28 | Type 'servos help ' for help on a specific subcommand. 29 | 30 | Available subcommands: 31 | find 32 | install 33 | makecmd 34 | makeproject 35 | makeservice 36 | runserver 37 | ``` 38 | 39 | 在servos中,有一些是全局性的命令,有一些是由某个service提供的命令。因此当你在一个 40 | project目录下运行servos命令时,它会根据当前project所安装的servos来显示一个完整的命令清单。 41 | 上面的示例只显示了在没有任何service时的全局命令。 42 | 43 | 如果想看单个命令的帮助信息,可以执行: 44 | 45 | 46 | ``` 47 | #> servos help runserver 48 | Usage: servos runserver [options] 49 | 50 | Start a new development server. 51 | 52 | Options: 53 | -b, --background If run in background. Default is False. 54 | ``` 55 | 56 | ### 常用全局选项说明 57 | 58 | 除了命令,servos还提供了全局可用的参数,它与单个命令自已的参数不同,它是对所有命令都可以使用的参数。 59 | 60 | ### runserver 61 | 62 | 启动开发服务器: 63 | 64 | 65 | ``` 66 | Usage: servos runserver [options] 67 | 68 | Start a new development server. 69 | 70 | Options: 71 | -b, --background If run in background. Default is False. 72 | ``` 73 | 74 | 示例: 75 | 76 | ``` 77 | servos runserver #启动服务 78 | ``` 79 | 80 | 81 | ### makeproject 82 | 83 | 生成一个project框架,它将自动按给定的名字生成一个project目录,同时包含有初始子目录和文件。 84 | 85 | 86 | ``` 87 | Usage: servos makeproject projectname 88 | ``` 89 | 90 | 示例: 91 | 92 | ``` 93 | servos makeproject project 94 | ``` 95 | 96 | 创建project项目目录。 97 | 98 | 99 | ### makeservice 100 | 101 | 生成一个service框架,它将自动按给定的名字生成一个service目录,同时包含有初始子目录和文件。 102 | 103 | 104 | ``` 105 | Usage: servos makeservice servicename 106 | ``` 107 | 108 | 示例: 109 | 110 | ``` 111 | servos makeservice Hello 112 | ``` 113 | 114 | 创建Hello服务。如果当前目前下有services目录,则将在services目录下创建一个Hello的目录, 115 | 并带有初始的文件和结构。如果当前目前下没有services目录,则直接创建Hello的目录。 116 | 117 | 118 | ### makecmd 119 | 120 | 向指定的service或当前目录下生成一个commands.py模板。 121 | 122 | ``` 123 | Usage: servos makecmd [servicename, ...] 124 | ``` 125 | 126 | 示例: 127 | 128 | ``` 129 | servos makecmd Hello 130 | ``` 131 | 132 | ### find 133 | 134 | 查找对象,目前仅支持查找配置项。 135 | 136 | 137 | ``` 138 | Usage: servos find -o option 139 | ``` 140 | 141 | 142 | 示例: 143 | 144 | ``` 145 | servos find -o GLOBAL/DEFAULT_ENCODING 146 | ``` 147 | 148 | ### install 149 | 150 | ``` 151 | Usage: servos install [servicename,...] 152 | ``` 153 | 154 | 执行在项目目录下或service目录下的requirements.txt。如果不指定servicename,则是扫描整个项 155 | 目,如果指定servicename,则只扫描指定service下的requirements.txt。 156 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | ### 配置settings 2 | 3 | #### 加载顺序 4 | 5 | 配置信息一般是放在 settings.ini 文件中的,目前有以下几个级别的 settings.ini 信息: 6 | 7 | ```python 8 | default_settings.ini #框架缺省的配置 9 | services/ 10 | service/settings.ini #服务缺省的配置 11 | settings.ini #项目配置 12 | local_settings.ini #项目的本地配置 13 | ``` 14 | 配置的加载顺序是从上到下的。对于service下的 settings.ini 文件, 15 | 它会按照 services 在 INSTALLED_SERVICES 中的定义顺序来处理。 16 | 17 | #### 变量合并与替换 18 | 配置文件有先后加载顺序,后加载的项一旦发现有重名的情况,会进行以下特殊的处理: 19 | 20 | 1. 如果值为list, dict, set,则进行合并处理,即对于list,执行extend(), 21 | 对于dict执行update,同时如果dict的值为dict或list,则会进行递归合并处理。 22 | 2. 如果是其它的值,则进行替換,即后面定义的值覆盖前面定义的值 23 | 3. 如果写法为: 24 | 25 | ``` 26 | name <= value 27 | ``` 28 | 29 | 则不管value是什么都将进行替換。 30 | 31 | 32 | ## 基本格式 33 | 34 | settings.ini的写法是类ini格式,但是和标准的ini有所区别: 35 | 36 | 37 | 基本写法是: 38 | ``` 39 | [section] 40 | name = value 41 | ``` 42 | 43 | section -- 44 | 节的名字,它应该是一个合法的标识符,即开始为字母或 `_` ,后续可 45 | 以是字母或 `_` 或数字。大小写敏感。 46 | 47 | name -- 48 | key,不应包含 '=',可以不是标识符。name不能与Ini初始化时传入的env中的key重名,如果有会报错。 49 | 50 | value -- 51 | 值,并且是符合python语法的,因此你可以写list, dict, tuple, 三重字符串和 52 | 其它的标准的python的类型。可以在符合python语法的情况下占多行。也可以是简单 53 | 的表达式。 54 | 55 | 56 | 1. 不支持多级,只支持两级处理 57 | 2. 注释写法是第一列为 `'#'` 58 | 3. 可以在文件头定义 `#coding=utf-8` 来声明此文件的编码,以便可以使用 `u'中文'` 这样的内容。 59 | 60 | 61 | settings中的value只能定义基本的 Python 数据结构和表达式,因为它们将使用 eval 来执行,所以不能随意写代码,也不能导入其它的模块。 62 | 63 | ## 配置项引用 64 | 65 | 在定义 value ,我们可以直接引入前面已经定义好的配置项,有两种类型: 66 | 67 | 1. section内部引用,即引用的配置项是在同一节中当前配置项之前定义的其它的项,如: 68 | 69 | ``` 70 | [DEFAULT] 71 | a = 'http://abc.com' 72 | b = a + '/index' 73 | ``` 74 | 75 | 一个section内部引用,直接在 value 部分写对应的配置项名称即可。但是要注意,因为 key 并不要求一定是标识符,所以,如果 key 的定义不是标识符,则直接引用的话,可能会因为 eval 执行时出错。这样就要使用第二种方法。 76 | 77 | 2. 跨section引用。它需要在配置项前面添加所在的 section 的名称,如: 78 | 79 | ``` 80 | [DEFAULT] 81 | a = 'http://abc.com' 82 | [OTHER] 83 | b = DEFAULT.a + '/index' 84 | c = DEFAULT['a'] + '/index' 85 | d = OTHER.b + '/test' 86 | ``` 87 | 88 | 上面示例中, `b` 引用了 `DEFAULT` 中的配置项。 `c` 是采用dict下标的写法,适用于 key 不是标识符的情况。 `d` 则是以添加 section 名字的方式来实现section内部引用。 89 | 90 | ## 字符串引用扩展 91 | 92 | 引用方式一般是为了解决某个值在多个配置项中重复出现,而采用的减化手段。一般可以使用表达式的方法进行值的加工。如果是字符串,还可以直接在字符串中定义如 `{{expr}}` 这样的格式串来引用配置项的值。如: 93 | 94 | ``` 95 | [DEFAULT] 96 | a = 'http://abc.com' 97 | b = '{{DEFAULT.a}}/index' 98 | ``` 99 | 100 | 在 ``{{}}`` 内的值会自动被替換为相应的配置项的值。这样有时写起来比表达式可能更方便。 101 | 102 | ## 环境变量的引用扩展 103 | 104 | 有时需要在配置中引入一些环境变量,如很多云环境都是把数据库等的参数放在环境变量中。 105 | 因此,如果你需要在配置文件中引入这些变量,可以使用: 106 | 107 | ``` 108 | a = '${MYSQL_HOST}:${MYSQL_PORT}' 109 | a = $MYSQL_PORT 110 | ``` 111 | 112 | 113 | ## settings的使用 114 | 115 | settings在读取后会生成一个对象,要先获得这个对象再使用它。在需要使用settings的代码中通过导入来使用settings对象。 116 | 117 | ``` 118 | from servos import settings 119 | ``` 120 | 121 | 有了settings对象,我们就可以调用它的方法和属性来引用配置文件中的各个值了。settings对象,你可以理解为一个二级的字典或二级的对象树。 122 | 如果key或section名都是标识符,通常情况下使用 `.` 的属性引用方式就可以了,不然可以象字典一样使用下标或 `get()` 等方法来使用。 123 | 常见的使用方式有: 124 | 125 | 1. `settings[section][key]` 以字典的形式来处理 126 | 1. `settings.get_var('section/key', default=None)` 这种形式可以写一个查找的路径的形式 127 | 1. `settings.section` 或 `settings.section.key` 以 `.` 的形式来引用section和key,不过要求section和key是标识符,且不能是保留字。 128 | 1. `for k, v in settings.section.items()` 可以把settings.section当成一个字典来使用,因此字典的许多方法都可以使用,如 in, has 之类的。 129 | 130 | ## 关于Lazy的处理说明 131 | 132 | Lazy的处理是与配置项的引用有关。考虑以下场景: 133 | 134 | * 某个service定义了一串配置项,如: 135 | 136 | ``` 137 | [PARA] 138 | domain = 'http://localhost:8000' 139 | login_url = domain + '/login' 140 | ``` 141 | * 然后当部署到某个环境中时,用户希望在local_settings.ini中覆盖上面的domain的值 142 | 为实际的地址。 143 | 144 | 这时会有这样的问题:在解析service的settings.ini文件时,domain的值已经解析出来了, 145 | 因此在处理 login_url 时,它的值也就固定下来了。等 local_settings.ini 再覆盖domain, 146 | login_url 已经不会再重新计算了。 147 | 148 | 所以为了解决因为多个settings.ini按顺序导入,但是变量提前计算的问题,引入了Lazy的处理方式。 149 | 即,在读取多个settings.ini时,并不计算配置项的值,只是放到这个配置项对应的数组中,等全部读取完毕, 150 | 由框架主动调用settings的freeze()方法开始对所有未计算的值进行求值。通过这样的方法就延迟了求值的时间, 151 | 保证了最终想到的结果。 152 | 153 | ## 重要配置参数说明 154 | 155 | 以下按不同的节(section)来区分 156 | 157 | 158 | ### GLOBAL 159 | 160 | 161 | ``` 162 | [GLOBAL] 163 | INSTALLED_SERVICES = [ 164 | 'message_collector', 165 | 'cache_manager', 166 | ] 167 | ``` 168 | 169 | INSTALLED_SERVICES 为要加载的服务列表。框架会根据此配置加载启动指定服务。 170 | 171 | ### LOG 172 | 173 | 详情参见 [日志处理说明](log.md) 。 174 | 175 | 176 | ### FUNCTIONS 177 | 178 | 用于定义公共的一些函数,例如: 179 | 180 | 181 | ``` 182 | [FUNCTIONS] 183 | encode = 'codec_plugin.encode' 184 | ``` 185 | 186 | 在此定义之后,可以使用如下方式引用: 187 | 188 | 189 | ``` 190 | from servos import functions 191 | functions.encode(message) 192 | ``` 193 | 194 | 195 | ### BINDS 196 | 197 | 用于绑定某个信号的配置,例如: 198 | 199 | 200 | ``` 201 | [BINDS] 202 | cache_manager.notify_dispatch = 'notify_dispatch', 'cache_manager.add_messages' 203 | ``` 204 | 205 | 在配置中,每个绑定的函数应有一个名字,在最简单的情况下,可以省略名字,函数名就与绑定名相同。 206 | 207 | BINDS有三种定义形式: 208 | 209 | 210 | ```python 211 | function = topic #最简单情况,函数名与绑定名相同,topic是对应的信号 212 | bind_name = topic, function #给出信号和函数路径 213 | bind_name = topic, function, {kwargs} #给出信号,函数路径和参数(字典形式) 214 | ``` 215 | 216 | 其中function中是函数路径,比如 `servicename.model.function_name` , 217 | 利用这种形式,框架可以根据 `servicename.model` 来导入函数。 218 | 219 | 上面的 `bind_name` 没有特别的作用,只是要求唯一,一方面利用它可以实现: 220 | 一个函数可以同时处理多个 topic 的情况,只要定义不同的 `bind_name` 即可。另一方面, 221 | 可以起到替換的作用,如果某个绑定不想再继续使用或替换为其它的配置, 222 | 可以写一个同名的`bind_name` 让后面的替換前面的。 223 | 224 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is here because many Platforms as a Service look for 2 | # requirements.txt in the root directory of a project. 3 | 4 | # PBR should always appear first 5 | pbr>=1.8 6 | 7 | Babel>=2.3 8 | six>=1.9.0 9 | 10 | # i18n 11 | i18n==0.2 12 | 13 | jsonpath_rw_ext==1.0.0 14 | # cache 15 | pylibmc==1.5.1 16 | 17 | # openstack oslo 18 | oslo.service==1.16.0 19 | oslo.messaging==5.10.0 20 | -------------------------------------------------------------------------------- /servos/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __all__ = [ 3 | 'ServiceBase', 4 | 'entry', 5 | 'ServosError', 6 | 'functions', 7 | 'settings', 8 | 'application', 9 | 'Middleware', 10 | 'dispatch', 11 | 'version', 12 | 'get_services', 13 | 'get_service_dir', 14 | ] 15 | version = '0.0.1' 16 | 17 | import sys 18 | reload(sys) 19 | sys.setdefaultencoding('utf-8') #@UndefinedVariable 20 | 21 | import eventlet 22 | eventlet.monkey_patch() 23 | 24 | 25 | class ServosError(Exception): 26 | pass 27 | 28 | 29 | class Middleware(object): 30 | ORDER = 500 31 | 32 | def __init__(self, application, settings): 33 | self.application = application 34 | self.settings = settings 35 | 36 | import servos.core.dispatch as dispatch 37 | from servos.core.service import ServiceBase 38 | from servos.core.service import entry 39 | from servos.core.simpleframe import ( 40 | functions, settings, application, get_service_dir, get_services) 41 | -------------------------------------------------------------------------------- /servos/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knitmesh/servos-framework/45fdc04580890c3aac039c023f104ce8dc00af08/servos/contrib/__init__.py -------------------------------------------------------------------------------- /servos/contrib/i18n/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knitmesh/servos-framework/45fdc04580890c3aac039c023f104ce8dc00af08/servos/contrib/i18n/__init__.py -------------------------------------------------------------------------------- /servos/contrib/i18n/middle_i18n.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from servos import Middleware 5 | from servos.i18n import set_language 6 | 7 | 8 | def get_language_from_request(request, settings): 9 | # 语言参数名称 10 | request_lang_key = settings.get_var('I18N/REQUEST_LANG_KEY') 11 | if request_lang_key: 12 | lang = request.get(request_lang_key) 13 | if lang: 14 | return lang 15 | 16 | # 默认语言 17 | return settings.I18N.get('LANGUAGE_CODE') 18 | 19 | 20 | class I18nMiddle(Middleware): 21 | 22 | def process_request(self, request): 23 | lang = get_language_from_request(request, self.settings) 24 | if lang: 25 | set_language(lang) 26 | -------------------------------------------------------------------------------- /servos/contrib/i18n/settings.ini: -------------------------------------------------------------------------------- 1 | [I18N] 2 | LANGUAGE_COOKIE_NAME = 'servos_language' 3 | LANGUAGE_CODE = 'en' 4 | LOCALE_DIRS = [] 5 | REQUEST_LANG_KEY = 'REQUEST_LANG' 6 | -------------------------------------------------------------------------------- /servos/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knitmesh/servos-framework/45fdc04580890c3aac039c023f104ce8dc00af08/servos/core/__init__.py -------------------------------------------------------------------------------- /servos/core/commands.py: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # This module is desired by Django 3 | ################################################################## 4 | import sys 5 | import os 6 | from optparse import make_option, OptionParser, IndentedHelpFormatter 7 | import servos 8 | import logging 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | def handle_default_options(options): 13 | """ 14 | Include any default options that all commands should accept here 15 | so that ManagementUtility can handle them before searching for 16 | user commands. 17 | 18 | """ 19 | if options.pythonpath: 20 | sys.path.insert(0, options.pythonpath) 21 | 22 | 23 | class CommandError(Exception): 24 | """ 25 | Exception class indicating a problem while executing a management 26 | command. 27 | 28 | If this exception is raised during the execution of a management 29 | command, it will be caught and turned into a nicely-printed error 30 | message to the appropriate output stream (i.e., stderr); as a 31 | result, raising this exception (with a sensible description of the 32 | error) is the preferred way to indicate that something has gone 33 | wrong in the execution of a command. 34 | 35 | """ 36 | pass 37 | 38 | 39 | def get_answer(message, answers='Yn', default='Y', quit=''): 40 | """ 41 | Get an answer from stdin, the answers should be 'Y/n' etc. 42 | If you don't want the user can quit in the loop, then quit should be None. 43 | """ 44 | if quit and quit not in answers: 45 | answers = answers + quit 46 | 47 | message = message + '(' + '/'.join(answers) + ')[' + default + ']:' 48 | ans = raw_input(message).strip().upper() 49 | if default and not ans: 50 | ans = default.upper() 51 | while ans not in answers.upper(): 52 | ans = raw_input(message).strip().upper() 53 | if quit and ans == quit.upper(): 54 | print "Command be cancelled!" 55 | sys.exit(0) 56 | return ans 57 | 58 | 59 | def get_input(prompt, default=None, choices=None, option_value=None): 60 | """ 61 | If option_value is not None, then return it. Otherwise get the result from 62 | input. 63 | """ 64 | if option_value is not None: 65 | return option_value 66 | 67 | choices = choices or [] 68 | while 1: 69 | r = raw_input(prompt + ' ').strip() 70 | if not r and default is not None: 71 | return default 72 | if choices: 73 | if r not in choices: 74 | r = None 75 | else: 76 | break 77 | else: 78 | break 79 | return r 80 | 81 | 82 | class CommandMetaclass(type): 83 | 84 | def __init__(cls, name, bases, dct): 85 | option_list = list(dct.get('option_list', [])) 86 | for c in bases: 87 | if hasattr(c, 'option_list') and isinstance(c.option_list, list): 88 | option_list.extend(c.option_list) 89 | cls.option_list = option_list 90 | 91 | 92 | class Command(object): 93 | __metaclass__ = CommandMetaclass 94 | 95 | option_list = () 96 | help = '' 97 | args = '' 98 | check_services_dirs = True 99 | check_services = False 100 | 101 | def create_parser(self, prog_name, subcommand): 102 | """ 103 | Create and return the ``OptionParser`` which will be used to 104 | parse the arguments to this command. 105 | 106 | """ 107 | return OptionParser(prog=prog_name, 108 | usage=self.usage(subcommand), 109 | version='', 110 | add_help_option=False, 111 | option_list=self.option_list) 112 | 113 | def get_version(self): 114 | return "servos version is %s" % servos.version 115 | 116 | def usage(self, subcommand): 117 | """ 118 | Return a brief description of how to use this command, by 119 | default from the attribute ``self.help``. 120 | 121 | """ 122 | if len(self.option_list) > 0: 123 | usage = '%%prog %s [options] %s' % (subcommand, self.args) 124 | else: 125 | usage = '%%prog %s %s' % (subcommand, self.args) 126 | if self.help: 127 | return '%s\n\n%s' % (usage, self.help) 128 | else: 129 | return usage 130 | 131 | def print_help(self, prog_name, subcommand): 132 | """ 133 | Print the help message for this command, derived from 134 | ``self.usage()``. 135 | 136 | """ 137 | parser = self.create_parser(prog_name, subcommand) 138 | parser.print_help() 139 | 140 | def get_services(self, global_options): 141 | from servos.core.simpleframe import get_services 142 | return get_services(global_options.services_dir, 143 | settings_file=global_options.settings, local_settings_file=global_options.local_settings) 144 | 145 | def get_application(self, global_options): 146 | from servos.manage import make_simple_application 147 | 148 | return make_simple_application(services_dir=global_options.services_dir, 149 | settings_file=global_options.settings, 150 | local_settings_file=global_options.local_settings 151 | ) 152 | 153 | def run_from_argv(self, prog, subcommand, global_options, argv): 154 | """ 155 | Set up any environment changes requested, then run this command. 156 | 157 | """ 158 | self.prog_name = prog 159 | parser = self.create_parser(prog, subcommand) 160 | options, args = parser.parse_args(argv) 161 | self.execute(args, options, global_options) 162 | 163 | def execute(self, args, options, global_options): 164 | from servos.utils.common import check_services_dir 165 | 166 | # add services_dir to global_options and insert it to sys.path 167 | if global_options.services_dir not in sys.path: 168 | sys.path.insert(0, global_options.services_dir) 169 | 170 | if self.check_services_dirs: 171 | check_services_dir(global_options.services_dir) 172 | if self.check_services and args: # then args should be services 173 | all_services = self.get_services(global_options) 174 | services = args 175 | args = [] 176 | for p in services: 177 | if p not in all_services: 178 | print 'Error: ServiceName %s is not a valid service' % p 179 | sys.exit(1) 180 | else: 181 | args.append(p) 182 | try: 183 | self.handle(options, global_options, *args) 184 | except CommandError as e: 185 | log.exception(e) 186 | sys.exit(1) 187 | 188 | def handle(self, options, global_options, *args): 189 | """ 190 | The actual logic of the command. Subclasses must implement 191 | this method. 192 | 193 | """ 194 | raise NotImplementedError() 195 | 196 | 197 | class NewFormatter(IndentedHelpFormatter): 198 | 199 | def format_heading(self, heading): 200 | return "%*s%s:\n" % (self.current_indent, "", 'Global Options') 201 | 202 | 203 | class NewOptionParser(OptionParser): 204 | 205 | def _process_args(self, largs, rargs, values): 206 | while rargs: 207 | arg = rargs[0] 208 | longarg = False 209 | try: 210 | if arg[0:2] == "--" and len(arg) > 2: 211 | # process a single long option (possibly with value(s)) 212 | # the superclass code pops the arg off rargs 213 | longarg = True 214 | self._process_long_opt(rargs, values) 215 | elif arg[:1] == "-" and len(arg) > 1: 216 | # process a cluster of short options (possibly with 217 | # value(s) for the last one only) 218 | # the superclass code pops the arg off rargs 219 | self._process_short_opts(rargs, values) 220 | else: 221 | # it's either a non-default option or an arg 222 | # either way, add it to the args list so we can keep 223 | # dealing with options 224 | del rargs[0] 225 | raise Exception 226 | except: 227 | if longarg: 228 | if '=' in arg: 229 | del rargs[0] 230 | largs.append(arg) 231 | 232 | 233 | class CommandManager(Command): 234 | usage_info = "%prog [global_options] [subcommand [options] [args]]" 235 | 236 | def __init__(self, argv=None, commands=None, prog_name=None, global_options=None): 237 | self.argv = argv 238 | self.prog_name = prog_name or os.path.basename(self.argv[0]) 239 | self.commands = commands 240 | self.global_options = global_options 241 | 242 | def get_commands(self, global_options): 243 | if callable(self.commands): 244 | commands = self.commands(global_options) 245 | else: 246 | commands = self.commands 247 | return commands 248 | 249 | def print_help_info(self, global_options): 250 | """ 251 | Returns the script's main help text, as a string. 252 | """ 253 | usage = [ 254 | '', "Type '%s help ' for help on a specific subcommand." % self.prog_name, ''] 255 | usage.append('Available subcommands:') 256 | commands = self.get_commands(global_options).keys() 257 | commands.sort() 258 | for cmd in commands: 259 | usage.append(' %s' % cmd) 260 | return '\n'.join(usage) 261 | 262 | def fetch_command(self, global_options, subcommand): 263 | """ 264 | Tries to fetch the given subcommand, printing a message with the 265 | appropriate command called from the command line (usually 266 | "servos") if it can't be found. 267 | """ 268 | commands = self.get_commands(global_options) 269 | try: 270 | klass = commands[subcommand] 271 | except KeyError: 272 | sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\nMany commands will only run at project directory, maybe the directory is not right.\n" % 273 | (subcommand, self.prog_name)) 274 | sys.exit(1) 275 | return klass 276 | 277 | def execute(self, callback=None): 278 | """ 279 | Given the command-line arguments, this figures out which subcommand is 280 | being run, creates a parser appropriate to that command, and runs it. 281 | """ 282 | # Preprocess options to extract --settings and --pythonpath. 283 | # These options could affect the commands that are available, so they 284 | # must be processed early. 285 | parser = NewOptionParser(prog=self.prog_name, 286 | usage=self.usage_info, 287 | formatter=NewFormatter(), 288 | add_help_option=False, 289 | option_list=self.option_list) 290 | 291 | if not self.global_options: 292 | global_options, args = parser.parse_args(self.argv) 293 | global_options.services_dir = os.path.normpath( 294 | global_options.services_dir) 295 | handle_default_options(global_options) 296 | args = args[1:] 297 | else: 298 | global_options = self.global_options 299 | args = self.argv 300 | 301 | global_options.settings = global_options.settings or os.environ.get( 302 | 'SETTINGS', 'settings.ini') 303 | global_options.local_settings = global_options.local_settings or os.environ.get( 304 | 'LOCAL_SETTINGS', 'local_settings.ini') 305 | 306 | if callback: 307 | callback(global_options) 308 | 309 | def print_help(global_options): 310 | parser.print_help() 311 | sys.stderr.write(self.print_help_info(global_options) + '\n') 312 | sys.exit(0) 313 | 314 | if len(args) == 0: 315 | if global_options.version: 316 | print self.get_version() 317 | sys.exit(0) 318 | else: 319 | print_help(global_options) 320 | sys.exit(1) 321 | 322 | try: 323 | subcommand = args[0] 324 | except IndexError: 325 | subcommand = 'help' # Display help if no arguments were given. 326 | 327 | if subcommand == 'help': 328 | if len(args) > 1: 329 | command = self.fetch_command(global_options, args[1]) 330 | if issubclass(command, CommandManager): 331 | cmd = command(['help'], None, '%s %s' % ( 332 | self.prog_name, args[1]), global_options=global_options) 333 | cmd.execute() 334 | else: 335 | command().print_help(self.prog_name, args[1]) 336 | sys.exit(0) 337 | else: 338 | print_help(global_options) 339 | if global_options.help: 340 | print_help(global_options) 341 | else: 342 | command = self.fetch_command(global_options, subcommand) 343 | if issubclass(command, CommandManager): 344 | cmd = command(args[1:], None, '%s %s' % ( 345 | self.prog_name, subcommand), global_options=global_options) 346 | cmd.execute() 347 | else: 348 | cmd = command() 349 | cmd.run_from_argv( 350 | self.prog_name, subcommand, global_options, args[1:]) 351 | 352 | 353 | class ApplicationCommandManager(CommandManager): 354 | option_list = ( 355 | make_option('--help', action='store_true', dest='help', 356 | help='show this help message and exit.'), 357 | make_option('-v', '--verbose', action='store_true', 358 | help='Output the result in verbose mode.'), 359 | make_option('-s', '--settings', dest='settings', 360 | default='', 361 | help='Settings file name. Default is "settings.ini".'), 362 | make_option('-y', '--yes', dest='yes', action='store_true', 363 | help='Automatic yes to prompt.'), 364 | make_option('-L', '--local_settings', dest='local_settings', 365 | default='', 366 | help='Local settings file name. Default is "local_settings.ini".'), 367 | make_option('-d', '--dir', dest='services_dir', 368 | default='services', 369 | help='Your services directory, default is "%default".'), 370 | make_option('--pythonpath', default='', 371 | help='A directory to add to the Python path, e.g. "/home/myproject".'), 372 | make_option('--version', action='store_true', dest='version', 373 | help="show program's version number and exit."), 374 | ) 375 | help = '' 376 | args = '' 377 | 378 | 379 | def execute_command_line(argv=None, commands=None, prog_name=None, callback=None): 380 | m = ApplicationCommandManager(argv, commands, prog_name) 381 | m.execute(callback) 382 | 383 | if __name__ == '__main__': 384 | execute_command_line(sys.argv) 385 | -------------------------------------------------------------------------------- /servos/core/default_settings.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | DEBUG = False 3 | DEFAULT_ENCODING = 'utf-8' 4 | TIME_ZONE = None 5 | LOCAL_TIME_ZONE = None 6 | 7 | 8 | [LOG] 9 | #level, filename, filemode, datefmt, format can be used in logging.basicConfig 10 | level = 'info' 11 | format = "[%(levelname)s %(name)s %(asctime)-15s %(filename)s,%(lineno)d] %(message)s" 12 | 13 | 14 | [LOG.Loggers] 15 | ROOT={'handlers':['Full']} 16 | 17 | 18 | [LOG.Handlers] 19 | Full = {'format':'format_full','class':'logging.handlers.RotatingFileHandler','args':('default.log','w',10000000,15)} 20 | Simple = {'format':'format_simple'} 21 | Package = {'format':'format_package'} 22 | 23 | #defines all log fomatters 24 | [LOG.Formatters] 25 | format_full = "[%(levelname)s %(name)s %(asctime)-15s %(filename)s,%(lineno)d] %(message)s" 26 | format_simple = "[%(levelname)s] %(message)s" 27 | format_package = "[%(levelname)s %(name)s] %(message)s" 28 | 29 | 30 | [FUNCTIONS] 31 | 32 | [GLOBAL_OBJECTS] 33 | 34 | [MIDDLEWARES] 35 | -------------------------------------------------------------------------------- /servos/core/dispatch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import inspect 3 | from servos.utils.common import import_attr 4 | 5 | __all__ = ['HIGH', 'MIDDLE', 'LOW', 'bind', 'call', 6 | 'get', 'unbind', 'call_once', 'get_once'] 7 | 8 | HIGH = 1 # plugin high 9 | MIDDLE = 2 10 | LOW = 3 11 | 12 | _receivers = {} 13 | _called = {} 14 | 15 | 16 | class NoneValue(object): 17 | pass 18 | 19 | 20 | def bind(topic, signal=None, kind=MIDDLE, nice=-1): 21 | """ 22 | This is a decorator function, so you should use it as: 23 | 24 | @bind('init') 25 | def process_init(a, b): 26 | ... 27 | """ 28 | def f(func): 29 | if topic not in _receivers: 30 | receivers = _receivers[topic] = [] 31 | else: 32 | receivers = _receivers[topic] 33 | 34 | if nice == -1: 35 | if kind == MIDDLE: 36 | n = 500 37 | elif kind == HIGH: 38 | n = 100 39 | else: 40 | n = 900 41 | else: 42 | n = nice 43 | if callable(func): 44 | func_name = func.__module__ + '.' + func.__name__ 45 | func = func 46 | else: 47 | func_name = func 48 | func = None 49 | _f = (n, {'func': func, 'signal': signal, 'func_name': func_name}) 50 | receivers.append(_f) 51 | return func 52 | return f 53 | 54 | 55 | def unbind(topic, func): 56 | """ 57 | Remove receiver function 58 | """ 59 | if topic in _receivers: 60 | receivers = _receivers[topic] 61 | for i in range(len(receivers) - 1, -1, -1): 62 | nice, f = receivers[i] 63 | if (callable(func) and f['func'] == func) or (f['func_name'] == func): 64 | del receivers[i] 65 | return 66 | 67 | 68 | def _test(kwargs, receiver): 69 | signal = kwargs.get('signal', None) 70 | f = receiver['func'] 71 | args = inspect.getargspec(f)[0] 72 | if 'signal' not in args: 73 | kwargs.pop('signal', None) 74 | _signal = receiver.get('signal') 75 | flag = True 76 | if _signal: 77 | if isinstance(_signal, (tuple, list)): 78 | if signal not in _signal: 79 | flag = False 80 | elif _signal != signal: 81 | flag = False 82 | return flag 83 | 84 | 85 | def call(sender, topic, *args, **kwargs): 86 | """ 87 | Invoke receiver functions according topic, it'll invoke receiver functions one by one, 88 | and it'll not return anything, so if you want to return a value, you should 89 | use get function. 90 | """ 91 | if topic not in _receivers: 92 | return NoneValue 93 | 94 | items = _receivers[topic] 95 | 96 | def _cmp(x, y): 97 | return cmp(x[0], y[0]) 98 | 99 | items.sort(_cmp) 100 | i = 0 101 | while i < len(items): 102 | nice, f = items[i] 103 | i = i + 1 104 | _f = f['func'] 105 | if not _f: 106 | try: 107 | _f = import_attr(f['func_name']) 108 | except (ImportError, AttributeError): 109 | logging.error("Can't import function %s" % f['func_name']) 110 | raise 111 | f['func'] = _f 112 | if callable(_f): 113 | kw = kwargs.copy() 114 | if not _test(kw, f): 115 | continue 116 | try: 117 | _f(sender, *args, **kw) 118 | except: 119 | func = _f.__module__ + '.' + _f.__name__ 120 | logging.exception( 121 | 'Calling dispatch point [%s] %s(%r, %r) error!' % (topic, func, args, kw)) 122 | raise 123 | else: 124 | raise Exception( 125 | "Dispatch point [%s] %r can't been invoked" % (topic, _f)) 126 | 127 | 128 | def call_once(sender, topic, *args, **kwargs): 129 | signal = kwargs.get('signal') 130 | if (topic, signal) in _called: 131 | return NoneValue 132 | else: 133 | call(sender, topic, *args, **kwargs) 134 | _called[(topic, signal)] = True 135 | 136 | 137 | def get(sender, topic, *args, **kwargs): 138 | """ 139 | Invoke receiver functions according topic, it'll invoke receiver functions one by one, 140 | and if one receiver function return non-None value, it'll return it and break 141 | the loop. 142 | """ 143 | if topic not in _receivers: 144 | return NoneValue 145 | 146 | items = _receivers[topic] 147 | 148 | def _cmp(x, y): 149 | return cmp(x[0], y[0]) 150 | 151 | items.sort(_cmp) 152 | for i in range(len(items)): 153 | nice, f = items[i] 154 | _f = f['func'] 155 | if not _f: 156 | try: 157 | _f = import_attr(f['func_name']) 158 | except ImportError: 159 | logging.error("Can't import function %s" % f['func_name']) 160 | raise 161 | f['func'] = _f 162 | if callable(_f): 163 | if not _test(kwargs, f): 164 | continue 165 | try: 166 | v = _f(sender, *args, **kwargs) 167 | except: 168 | func = _f.__module__ + '.' + _f.__name__ 169 | logging.exception( 170 | 'Calling dispatch point [%s] %s(%r,%r) error!' % (topic, func, args, kwargs)) 171 | raise 172 | if v is not None: 173 | return v 174 | else: 175 | raise "Dispatch point [%s] %r can't been invoked" % (topic, _f) 176 | 177 | 178 | def get_once(sender, topic, *args, **kwargs): 179 | signal = kwargs.get('signal') 180 | if (topic, signal) in _called: 181 | return _called[(topic, signal)] 182 | else: 183 | r = get(sender, topic, *args, **kwargs) 184 | _called[(topic, signal)] = r 185 | return r 186 | -------------------------------------------------------------------------------- /servos/core/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import signal 3 | import os 4 | import inspect 5 | import logging 6 | from oslo_service import threadgroup, systemd 7 | from oslo_service.service import ServiceBase as OsloServiceBase 8 | from oslo_service.service import Launcher, SignalHandler, SignalExit, Services 9 | from oslo_service.service import _is_sighup_and_daemon 10 | from servos import dispatch 11 | 12 | logger = logging.getLogger('servos.core.service') 13 | 14 | # 保存实例化的服务对象 15 | __services__ = [] 16 | 17 | 18 | def _check_service_base(service): 19 | if not isinstance(service, ServiceBase): 20 | raise TypeError("Service %(service)s must an instance of %(base)s!" 21 | % {'service': service, 'base': ServiceBase}) 22 | 23 | 24 | class ServiceBase(OsloServiceBase): 25 | """Service object for binaries running on hosts.""" 26 | 27 | def __init__(self, threads=1000): 28 | self.tg = threadgroup.ThreadGroup(threads) 29 | 30 | def reset(self): 31 | """Reset a service in case it received a SIGHUP.""" 32 | 33 | def start(self): 34 | """Start a service.""" 35 | 36 | def stop(self, graceful=False): 37 | """Stop a service. 38 | 39 | :param graceful: indicates whether to wait for all threads to finish 40 | or terminate them instantly 41 | """ 42 | self.tg.stop(graceful) 43 | 44 | def wait(self): 45 | """Wait for a service to shut down.""" 46 | self.tg.wait() 47 | 48 | 49 | class ServiceLauncher(Launcher): 50 | """Runs one or more service in a parent process.""" 51 | 52 | def __init__(self, conf): 53 | """Constructor. 54 | """ 55 | self.conf = conf 56 | self.services = Services() 57 | # self.backdoor_port = ( 58 | # eventlet_backdoor.initialize_if_enabled(self.conf)) 59 | self.signal_handler = SignalHandler() 60 | 61 | def launch_service(self, service, workers=1): 62 | """Load and start the given service. 63 | 64 | :param service: The service you would like to start, must be an 65 | instance of :class:`servos.core.service.ServiceBase` 66 | :param workers: This param makes this method compatible with 67 | ProcessLauncher.launch_service. It must be None, 1 or 68 | omitted. 69 | :returns: None 70 | 71 | """ 72 | if workers is not None and workers != 1: 73 | raise ValueError("Launcher asked to start multiple workers") 74 | _check_service_base(service) 75 | self.services.add(service) 76 | 77 | def stop(self): 78 | """Stop all services which are currently running. 79 | """ 80 | self.services.stop() 81 | 82 | def restart(self): 83 | """restart service. 84 | """ 85 | self.services.restart() 86 | 87 | def _graceful_shutdown(self, *args): 88 | self.signal_handler.clear() 89 | if (self.conf.graceful_shutdown_timeout and 90 | self.signal_handler.is_signal_supported('SIGALRM')): 91 | signal.alarm(self.conf.graceful_shutdown_timeout) 92 | self.stop() 93 | 94 | def _reload_service(self, *args): 95 | self.signal_handler.clear() 96 | raise SignalExit(signal.SIGHUP) 97 | 98 | def _fast_exit(self, *args): 99 | logger.info('Caught SIGINT signal, instantaneous exiting') 100 | os._exit(1) 101 | 102 | def _on_timeout_exit(self, *args): 103 | logger.info('Graceful shutdown timeout exceeded, ' 104 | 'instantaneous exiting') 105 | os._exit(1) 106 | 107 | def handle_signal(self): 108 | """Set self._handle_signal as a signal handler.""" 109 | self.signal_handler.add_handler('SIGTERM', self._graceful_shutdown) 110 | self.signal_handler.add_handler('SIGINT', self._fast_exit) 111 | self.signal_handler.add_handler('SIGHUP', self._reload_service) 112 | self.signal_handler.add_handler('SIGALRM', self._on_timeout_exit) 113 | 114 | def _wait_for_exit_or_signal(self): 115 | status = None 116 | signo = 0 117 | 118 | try: 119 | super(ServiceLauncher, self).wait() 120 | except SignalExit as exc: 121 | signame = self.signal_handler.signals_to_name[exc.signo] 122 | logger.info('Caught %s, exiting', signame) 123 | status = exc.code 124 | signo = exc.signo 125 | except SystemExit as exc: 126 | self.stop() 127 | status = exc.code 128 | except Exception: 129 | self.stop() 130 | return status, signo 131 | 132 | def wait(self): 133 | """Wait for a service to terminate and restart it on SIGHUP. 134 | 135 | :returns: termination status 136 | """ 137 | systemd.notify_once() 138 | self.signal_handler.clear() 139 | while True: 140 | self.handle_signal() 141 | status, signo = self._wait_for_exit_or_signal() 142 | if not _is_sighup_and_daemon(signo): 143 | break 144 | self.restart() 145 | 146 | super(ServiceLauncher, self).wait() 147 | return status 148 | 149 | 150 | def entry(name=None, * params, **kparams): 151 | ''' 152 | 服务入口装饰器 153 | args: 154 | name:服务别名 155 | ''' 156 | global __services__ 157 | 158 | def _init(cls, * args, **kwargs): 159 | if not callable(cls): 160 | raise Exception("entry should be callable") 161 | 162 | if inspect.isclass(cls): 163 | _name = name or cls._service_name or cls.__name__ 164 | 165 | cls._service_name = _name 166 | __services__.append(cls()) 167 | 168 | return cls 169 | 170 | return _init 171 | 172 | 173 | class AccessPolicy(object): 174 | """权限检查。默认以下划线( _ )开始的函数不对外暴露。 175 | """ 176 | 177 | def is_allowed(self, endpoint, method): 178 | # 加入扩展点,可以对权限检查进行扩展 179 | re = dispatch.get(self, 'access_policy_check', endpoint, method) 180 | if re == dispatch.NoneValue: 181 | return re 182 | else: 183 | return not method.startswith('_') 184 | -------------------------------------------------------------------------------- /servos/core/simpleframe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import logging 5 | 6 | import servos.core.dispatch as dispatch 7 | import servos.core.service as service 8 | from servos.utils import pyini 9 | from servos.utils.localproxy import LocalProxy, Global 10 | from servos.utils.common import pkg, log, import_attr, myimport, norm_path 11 | from servos import ServosError 12 | 13 | 14 | __service_dirs__ = {} 15 | 16 | __global__ = Global() 17 | __global__.settings = pyini.Ini(lazy=True) 18 | 19 | 20 | class NoSuchMethod(ServosError, AttributeError): 21 | "Raised if there is no endpoint which exposes the requested method." 22 | 23 | def __init__(self, service, method): 24 | msg = "Endpoint does not support method %s" % method 25 | super(NoSuchMethod, self).__init__(msg) 26 | self.service = service 27 | self.method = method 28 | 29 | 30 | class Finder(object): 31 | 32 | def __init__(self, section): 33 | self.__objects = {} 34 | self.__section = section 35 | 36 | def __contains__(self, name): 37 | if name in self.__objects: 38 | return True 39 | if name not in settings[self.__section]: 40 | return False 41 | else: 42 | return True 43 | 44 | def __getattr__(self, name): 45 | if name in self.__objects: 46 | return self.__objects[name] 47 | if name not in settings[self.__section]: 48 | raise ServosError("Object %s is not existed!" % name) 49 | obj = import_attr(settings[self.__section].get(name)) 50 | self.__objects[name] = obj 51 | return obj 52 | 53 | def __setitem__(self, name, value): 54 | if isinstance(value, (str, unicode)): 55 | value = import_attr(value) 56 | self.__objects[name] = value 57 | 58 | functions = Finder('FUNCTIONS') 59 | 60 | 61 | def get_service_dir(service): 62 | """ 63 | 获取服务目录 64 | """ 65 | path = __service_dirs__.get(service) 66 | if path is not None: 67 | return path 68 | else: 69 | p = service.split('.') 70 | try: 71 | path = pkg.resource_filename(p[0], '') 72 | except ImportError as e: 73 | log.error("Can't import service %s", p[0]) 74 | log.exception(e) 75 | path = '' 76 | if len(p) > 1: 77 | path = os.path.join(path, *p[1:]) 78 | 79 | __service_dirs__[service] = path 80 | return path 81 | 82 | 83 | def get_service_depends(service, existed_services=None, installed_services=None): 84 | ''' 85 | 获取当前服务依赖的服务集合 86 | :param service:当前服务 87 | :param existed_services:已经收集过了 88 | :param installed_services:当前启动的服务 89 | ''' 90 | installed_services = installed_services or [] 91 | if existed_services is None: 92 | s = set() 93 | else: 94 | s = existed_services 95 | 96 | if service in s: 97 | raise StopIteration 98 | 99 | configfile = os.path.join(get_service_dir(service), 'settings.ini') 100 | if os.path.exists(configfile): 101 | x = pyini.Ini(configfile) 102 | # 读取配置中的依赖信息 103 | services = x.get_var('DEPENDS/REQUIRED_SERVICES', []) 104 | for i in services: 105 | if i not in s and i not in installed_services: 106 | # 递归查询 107 | for j in get_service_depends(i, s, installed_services): 108 | yield j 109 | s.add(service) 110 | yield service 111 | 112 | 113 | def get_services(services_dir, 114 | settings_file='settings.ini', 115 | local_settings_file='local_settings.ini'): 116 | ''' 117 | 收集服务信息 118 | :param services_dir:服务目录 119 | :param settings_file:配置文件 120 | :param local_settings_file:local配置文件 121 | ''' 122 | inifile = norm_path(os.path.join(services_dir, settings_file)) 123 | services = [] 124 | visited = set() 125 | installed_services = [] 126 | if not os.path.exists(services_dir): 127 | return services 128 | if os.path.exists(inifile): 129 | x = pyini.Ini(inifile) 130 | if x: 131 | installed_services.extend(x.GLOBAL.get('INSTALLED_SERVICES', [])) 132 | 133 | local_inifile = norm_path(os.path.join(services_dir, local_settings_file)) 134 | if os.path.exists(local_inifile): 135 | x = pyini.Ini(local_inifile) 136 | if x and x.get('GLOBAL'): 137 | installed_services.extend(x.GLOBAL.get('INSTALLED_SERVICES', [])) 138 | 139 | for _service in installed_services: 140 | services.extend( 141 | list(get_service_depends(_service, visited, installed_services))) 142 | 143 | if not services and os.path.exists(services_dir): 144 | for p in os.listdir(services_dir): 145 | if os.path.isdir(os.path.join(services_dir, p)) and \ 146 | p not in ['.svn', 'CVS', '.git'] and \ 147 | not p.startswith('.') and \ 148 | not p.startswith('_'): 149 | services.append(p) 150 | 151 | return services 152 | 153 | 154 | def collect_settings(services_dir, settings_file='settings.ini', 155 | local_settings_file='local_settings.ini', default_settings=None): 156 | ''' 157 | 收集配置文件,命令行使用 158 | :param services_dir: 159 | :param settings_file: 160 | :param local_settings_file: 161 | :param default_settings: 162 | ''' 163 | 164 | services = get_services(services_dir, 165 | settings_file=settings_file, 166 | local_settings_file=local_settings_file) 167 | settings_file = os.path.join(services_dir, settings_file) 168 | local_settings_file = os.path.join(services_dir, local_settings_file) 169 | settings = [] 170 | default_settings_file = pkg.resource_filename( 171 | 'servos.core', 'default_settings.ini') 172 | settings.insert(0, default_settings_file) 173 | for p in services: 174 | # deal with settings 175 | inifile = os.path.join(get_service_dir(p), 'settings.ini') 176 | if os.path.exists(inifile): 177 | settings.append(inifile) 178 | 179 | if os.path.exists(settings_file): 180 | settings.append(settings_file) 181 | 182 | if os.path.exists(local_settings_file): 183 | settings.append(local_settings_file) 184 | return settings 185 | 186 | 187 | class Dispatcher(object): 188 | installed = False 189 | 190 | def __init__(self, 191 | services_dir='services', 192 | start=True, 193 | default_settings=None, 194 | settings_file='settings.ini', 195 | local_settings_file='local_settings.ini'): 196 | 197 | __global__.application = self 198 | self.debug = False 199 | self.default_settings = default_settings or {} 200 | self.settings_file = settings_file 201 | self.local_settings_file = local_settings_file 202 | self.service_manager = service.ServiceLauncher(settings) 203 | self.access_policy = service.AccessPolicy() 204 | if not Dispatcher.installed: 205 | self.init(services_dir) 206 | dispatch.call(self, 'startup_installed') 207 | 208 | if start: 209 | dispatch.call(self, 'startup') 210 | self.service_manager.wait() 211 | 212 | def init(self, services_dir): 213 | Dispatcher.services_dir = norm_path(services_dir) 214 | # 处理获得所有需要的服务 215 | Dispatcher.services = get_services(self.services_dir, 216 | self.settings_file, 217 | self.local_settings_file) 218 | # 收集服务信息(服务和配置文件路径) 219 | Dispatcher.modules = self.collect_modules() 220 | 221 | # 收集配置信息 222 | self.install_settings(self.modules['settings']) 223 | 224 | # 安装全局对象 225 | self.install_global_objects() 226 | 227 | # 安装扩展调用 228 | self.install_binds() 229 | 230 | dispatch.call(self, 'after_init_settings') 231 | 232 | Dispatcher.settings = settings 233 | self.debug = settings.GLOBAL.get('DEBUG', False) 234 | 235 | # 设置日志 236 | self.set_log() 237 | 238 | # 安装服务 239 | self.init_services() 240 | self.install_services(self.modules['services']) 241 | 242 | dispatch.call(self, 'after_init_services') 243 | # 安装middleware 244 | Dispatcher.middlewares = self.install_middlewares() 245 | 246 | # 设置状态,服务安装完成 247 | Dispatcher.installed = True 248 | 249 | def set_log(self): 250 | 251 | s = self.settings 252 | 253 | def _get_level(level): 254 | return getattr(logging, level.upper()) 255 | 256 | # get basic configuration 257 | config = {} 258 | for k, v in s.LOG.items(): 259 | if k in ['format', 'datefmt', 'filename', 'filemode']: 260 | config[k] = v 261 | 262 | if s.get_var('LOG/level'): 263 | config['level'] = _get_level(s.get_var('LOG/level')) 264 | logging.basicConfig(**config) 265 | 266 | if config.get('filename'): 267 | Handler = 'logging.FileHandler' 268 | if config.get('filemode'): 269 | _args = (config.get('filename'), config.get('filemode')) 270 | else: 271 | _args = (config.get('filename'),) 272 | else: 273 | Handler = 'logging.StreamHandler' 274 | _args = () 275 | 276 | # process formatters 277 | formatters = {} 278 | for f, v in s.get_var('LOG.Formatters', {}).items(): 279 | formatters[f] = logging.Formatter(v) 280 | 281 | # process handlers 282 | handlers = {} 283 | for h, v in s.get_var('LOG.Handlers', {}).items(): 284 | handler_cls = v.get('class', Handler) 285 | handler_args = v.get('args', _args) 286 | 287 | handler = import_attr(handler_cls)(*handler_args) 288 | if v.get('level'): 289 | handler.setLevel(_get_level(v.get('level'))) 290 | 291 | format = v.get('format') 292 | if format in formatters: 293 | handler.setFormatter(formatters[format]) 294 | elif format: 295 | fmt = logging.Formatter(format) 296 | handler.setFormatter(fmt) 297 | 298 | handlers[h] = handler 299 | 300 | # process loggers 301 | for logger_name, v in s.get_var('LOG.Loggers', {}).items(): 302 | if logger_name == 'ROOT': 303 | log = logging.getLogger('') 304 | else: 305 | log = logging.getLogger(logger_name) 306 | 307 | if v.get('level'): 308 | log.setLevel(_get_level(v.get('level'))) 309 | if 'propagate' in v: 310 | log.propagate = v.get('propagate') 311 | if 'handlers' in v: 312 | for h in v['handlers']: 313 | if h in handlers: 314 | log.addHandler(handlers[h]) 315 | else: 316 | raise ServosError("Log Handler %s is not defined yet!") 317 | elif 'format' in v: 318 | if v['format'] not in formatters: 319 | fmt = logging.Formatter(v['format']) 320 | else: 321 | fmt = formatters[v['format']] 322 | _handler = import_attr(Handler)(*_args) 323 | _handler.setFormatter(fmt) 324 | log.addHandler(_handler) 325 | 326 | def collect_modules(self, check_service=True): 327 | ''' 328 | 收集服务信息 329 | :param check_service: 330 | ''' 331 | modules = {} 332 | _services = set() 333 | _settings = [] 334 | 335 | # 核心配置文件,最先加载 336 | inifile = pkg.resource_filename('servos.core', 'default_settings.ini') 337 | _settings.insert(0, inifile) 338 | 339 | def enum_services(service_path, service_name, subfolder=None, pattern=None): 340 | if not os.path.exists(service_path): 341 | log.error("Can't found the service %s path, " 342 | "please check if the path is right", 343 | service_name) 344 | return 345 | 346 | for f in os.listdir(service_path): 347 | fname, ext = os.path.splitext(f) 348 | if os.path.isfile(os.path.join(service_path, f)) and \ 349 | ext in ['.py', '.pyc', '.pyo'] and \ 350 | fname != '__init__': 351 | if pattern: 352 | import fnmatch 353 | if not fnmatch.fnmatch(f, pattern): 354 | continue 355 | if subfolder: 356 | _services.add( 357 | '.'.join([service_name, subfolder, fname])) 358 | else: 359 | _services.add('.'.join([service_name, fname])) 360 | 361 | for p in self.services: 362 | path = get_service_dir(p) 363 | # deal with services 364 | if check_service: 365 | service_path = os.path.join(path, 'services') 366 | if os.path.exists(service_path) and os.path.isdir(service_path): 367 | enum_services(service_path, p, 'services') 368 | else: 369 | enum_services(path, p, pattern='service*') 370 | # deal with settings 371 | inifile = os.path.join(get_service_dir(p), 'settings.ini') 372 | 373 | if os.path.exists(inifile): 374 | _settings.append(inifile) 375 | 376 | set_ini = os.path.join(self.services_dir, self.settings_file) 377 | if os.path.exists(set_ini): 378 | _settings.append(set_ini) 379 | 380 | # local配置文件,最后被加载 381 | local_set_ini = os.path.join( 382 | self.services_dir, self.local_settings_file) 383 | if os.path.exists(local_set_ini): 384 | _settings.append(local_set_ini) 385 | 386 | modules['services'] = list(_services) 387 | modules['settings'] = _settings 388 | return modules 389 | 390 | def install_services(self, services): 391 | ''' 392 | 加载服务。加载具体服务类的文件 393 | :param services: 394 | ''' 395 | for s in services: 396 | try: 397 | myimport(s) 398 | except Exception as e: 399 | log.exception(e) 400 | 401 | for _service in service.__services__: 402 | if isinstance(_service, service.ServiceBase): 403 | self.service_manager.launch_service(_service) 404 | else: 405 | pass 406 | 407 | def init_services(self): 408 | ''' 409 | 初始化加载服务。主要是执行__init__中代码 410 | ''' 411 | for p in self.services: 412 | try: 413 | myimport(p) 414 | except ImportError, e: 415 | pass 416 | except BaseException, e: 417 | log.exception(e) 418 | 419 | def install_settings(self, s): 420 | ''' 421 | 加载配置信息。否遭settings对象 422 | :param s: 423 | ''' 424 | for v in s: 425 | settings.read(v) 426 | settings.update(self.default_settings) 427 | settings.freeze() 428 | 429 | # process FILESYSTEM_ENCODING 430 | if not settings.GLOBAL.FILESYSTEM_ENCODING: 431 | settings.GLOBAL.FILESYSTEM_ENCODING = sys.getfilesystemencoding( 432 | ) or settings.GLOBAL.DEFAULT_ENCODING 433 | 434 | def install_global_objects(self): 435 | """ 436 | 根据[GLOBAL_OBJECTS]中的配置,向servos模块中注入对象。 437 | 其他模块可以直接使用import来获取。例如: 438 | from servos import test_obj 439 | """ 440 | import servos 441 | for k, v in settings.GLOBAL_OBJECTS.items(): 442 | setattr(servos, k, import_attr(v)) 443 | 444 | def install_binds(self): 445 | ''' 446 | 安装扩展。 447 | # process DISPATCH hooks 448 | # BINDS format 449 | # func = topic #bind_name will be the same with function 450 | # bind_name = topic, func 451 | # bind_name = topic, func, {args} 452 | ''' 453 | d = settings.get('BINDS', {}) 454 | for bind_name, args in d.items(): 455 | if not args: 456 | continue 457 | is_wrong = False 458 | if isinstance(args, (tuple, list)): 459 | if len(args) == 2: 460 | dispatch.bind(args[0])(args[1]) 461 | elif len(args) == 3: 462 | if not isinstance(args[2], dict): 463 | is_wrong = True 464 | else: 465 | dispatch.bind(args[0], **args[2])(args[1]) 466 | else: 467 | is_wrong = True 468 | elif isinstance(args, (str, unicode)): 469 | dispatch.bind(args)(bind_name) 470 | else: 471 | is_wrong = True 472 | if is_wrong: 473 | log.error( 474 | 'BINDS definition should be "function=topic" or ' 475 | '"bind_name=topic, function" or ' 476 | '"bind_name=topic, function, {"args":value1,...}"') 477 | raise ServosError( 478 | 'BINDS definition [%s=%r] is not right' % (bind_name, args)) 479 | 480 | def install_middlewares(self): 481 | return self.sort_middlewares(settings.get('MIDDLEWARES', {}).values()) 482 | 483 | def sort_middlewares(self, middlewares): 484 | # middleware process 485 | # middleware can be defined as 486 | # middleware_name = middleware_class_path[, order] 487 | # middleware_name = will be skip 488 | m = [] 489 | for v in middlewares: 490 | if not v: 491 | continue 492 | 493 | order = None 494 | if isinstance(v, (list, tuple)): 495 | if len(v) > 2: 496 | raise ServosError( 497 | 'Middleware %r difinition is not right' % v) 498 | middleware_path = v[0] 499 | if len(v) == 2: 500 | order = v[1] 501 | else: 502 | middleware_path = v 503 | cls = import_attr(middleware_path) 504 | 505 | if order is None: 506 | order = getattr(cls, 'ORDER', 500) 507 | m.append((order, cls)) 508 | 509 | m.sort(cmp=lambda x, y: cmp(x[0], y[0])) 510 | 511 | return [x[1] for x in m] 512 | 513 | def internal_error(self, e): 514 | ''' 515 | 处理异常 516 | ''' 517 | log.exception(e) 518 | return e 519 | 520 | def dispatch(self, incoming): 521 | ''' 522 | 执行服务调用 523 | :param incoming:调用信息。字典对象。 524 | 应包含: 525 | service:要调用服务名 526 | method:要调用的方法名 527 | args:参数。字典类型 528 | 529 | ''' 530 | 531 | middlewares = self.middlewares 532 | 533 | try: 534 | 535 | response = None 536 | _inss = {} 537 | for cls in middlewares: 538 | if hasattr(cls, 'process_request'): 539 | ins = cls(self, settings) 540 | _inss[cls] = ins 541 | response = ins.process_request(incoming) 542 | if response is not None: 543 | break 544 | 545 | if response is None: 546 | try: 547 | 548 | for _service in service.__services__: 549 | 550 | service_name = incoming['service'] 551 | method = incoming['method'] 552 | args = incoming['args'] 553 | 554 | # todo: 可以增加服务接口版本的过滤 555 | if not (_service._service_name == service_name): 556 | continue 557 | 558 | if hasattr(_service, method): 559 | # 权限验证 560 | if self.access_policy.is_allowed(_service, method): 561 | response = self._do_dispatch( 562 | _service, method, args) 563 | break 564 | else: 565 | raise NoSuchMethod(service_name, method) 566 | 567 | except Exception, e: 568 | for cls in reversed(middlewares): 569 | if hasattr(cls, 'process_exception'): 570 | ins = _inss.get(cls) 571 | if not ins: 572 | ins = cls(self, settings) 573 | response = ins.process_exception(incoming, e) 574 | if response: 575 | break 576 | raise 577 | 578 | for cls in reversed(middlewares): 579 | if hasattr(cls, 'process_response'): 580 | ins = _inss.get(cls) 581 | if not ins: 582 | ins = cls(self, settings) 583 | response = ins.process_response(incoming, response) 584 | 585 | except Exception as e: 586 | if not self.settings.get_var('GLOBAL/DEBUG'): 587 | response = self.internal_error(e) 588 | else: 589 | # log.exception(e) 590 | raise 591 | 592 | return response 593 | 594 | def _do_dispatch(self, service, method, args): 595 | func = getattr(service, method) 596 | result = func(**args) 597 | return result 598 | 599 | def __call__(self, incoming): 600 | return self.dispatch(incoming) 601 | 602 | 603 | settings = LocalProxy(__global__, 'settings', pyini.Ini) 604 | application = LocalProxy(__global__, 'application', Dispatcher) 605 | -------------------------------------------------------------------------------- /servos/i18n/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __all__ = ['set_language', 'get_language', 3 | 'gettext_lazy', 'ugettext_lazy', 'pgettext_lazy', 4 | 'gettext', 'ugettext', 'pgettext'] 5 | 6 | import six 7 | 8 | 9 | class Trans(object): 10 | 11 | def __getattr__(self, real_name): 12 | from servos import settings 13 | if settings.USE_I18N: 14 | from servos.i18n import trans_real as trans 15 | else: 16 | from servos.i18n import trans_null as trans 17 | setattr(self, real_name, getattr(trans, real_name)) 18 | return getattr(trans, real_name) 19 | 20 | _trans = Trans() 21 | 22 | # The Trans class is no more needed, so remove it from the namespace. 23 | del Trans 24 | 25 | 26 | def set_language(lang): 27 | _trans.set_language(lang) 28 | 29 | 30 | def get_language(): 31 | _trans.get_language() 32 | 33 | 34 | def gettext(message): 35 | return _trans.gettext(message) 36 | 37 | 38 | def ugettext(message): 39 | return _trans.ugettext(message) 40 | 41 | 42 | def pgettext(context, message): 43 | return _trans.pgettext(context, message) 44 | 45 | -------------------------------------------------------------------------------- /servos/i18n/trans_null.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from servos import settings 3 | import six 4 | 5 | 6 | def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): 7 | # return six.text_type(s) 8 | if isinstance(s, six.text_type): 9 | return s 10 | try: 11 | if not isinstance(s, six.string_types): 12 | if hasattr(s, '__unicode__'): 13 | s = six.text_type(s) 14 | else: 15 | s = six.text_type(bytes(s), encoding, errors) 16 | else: 17 | # Note: We use .decode() here, instead of six.text_type(s, encoding, 18 | # errors), so that if s is a SafeBytes, it ends up being a 19 | # SafeText at the end. 20 | s = s.decode(encoding, errors) 21 | except UnicodeDecodeError as e: 22 | if not isinstance(s, Exception): 23 | raise e 24 | else: 25 | # If we get to here, the caller has passed in an Exception 26 | # subclass populated with non-ASCII bytestring data without a 27 | # working unicode method. Try to handle this without raising a 28 | # further exception by individually forcing the exception args 29 | # to unicode. 30 | s = ' '.join(force_text(arg, encoding, strings_only, errors) 31 | for arg in s) 32 | return s 33 | 34 | 35 | set_language = lambda x: None 36 | get_language = lambda: None 37 | 38 | 39 | def gettext(message): 40 | return message 41 | 42 | 43 | def ugettext(message): 44 | return force_text(gettext(message)) 45 | 46 | 47 | def pgettext(context, message): 48 | return ugettext(message) 49 | -------------------------------------------------------------------------------- /servos/i18n/trans_real.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from servos import application 3 | from servos import get_service_dir 4 | from servos import settings 5 | from servos.utils.common import pkg 6 | 7 | DEFAULT_LANGUAGE = settings.get_var('I18N/LANGUAGE_CODE', None) 8 | 9 | path = pkg.resource_filename('servos', '') 10 | localedir = ( 11 | # 各服务路径 12 | [get_service_dir(service_name) for service_name in application.services] + 13 | # 框架路径 14 | [path] 15 | ) -------------------------------------------------------------------------------- /servos/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from optparse import make_option 3 | import inspect 4 | import logging 5 | import os 6 | import sys 7 | 8 | from servos.core import simpleframe 9 | from servos.core.commands import Command, CommandManager 10 | 11 | 12 | __commands__ = {} 13 | 14 | log = logging.getLogger('servos.console') 15 | 16 | 17 | def get_commands(global_options): 18 | global __commands__ 19 | 20 | def check(c): 21 | return (inspect.isclass(c) and 22 | issubclass(c, Command) and 23 | c is not Command and 24 | c is not CommandManager) 25 | 26 | def find_mod_commands(mod): 27 | for name in dir(mod): 28 | c = getattr(mod, name) 29 | if check(c): 30 | register_command(c) 31 | 32 | def collect_commands(): 33 | from servos import get_services, get_service_dir 34 | from servos.utils.common import is_pyfile_exist 35 | 36 | services = get_services( 37 | global_options.services_dir, 38 | settings_file=global_options.settings, 39 | local_settings_file=global_options.local_settings) 40 | for f in services: 41 | path = get_service_dir(f) 42 | if is_pyfile_exist(path, 'commands'): 43 | m = '%s.commands' % f 44 | mod = __import__(m, fromlist=['*']) 45 | 46 | find_mod_commands(mod) 47 | 48 | collect_commands() 49 | return __commands__ 50 | 51 | 52 | def register_command(kclass): 53 | global __commands__ 54 | __commands__[kclass.name] = kclass 55 | 56 | 57 | def install_config(services_dir): 58 | from servos.utils import pyini 59 | # user can configure custom PYTHONPATH, so that servos can add these paths 60 | # to sys.path, and user can manage third party or public services in a separate 61 | # directory 62 | config_filename = os.path.join(services_dir, 'config.ini') 63 | if os.path.exists(config_filename): 64 | c = pyini.Ini(config_filename) 65 | paths = c.GLOBAL.get('PYTHONPATH', []) 66 | if paths: 67 | for p in reversed(paths): 68 | p = os.path.abspath(os.path.normpath(p)) 69 | if p not in sys.path: 70 | sys.path.insert(0, p) 71 | 72 | 73 | def make_application(services_dir='services', 74 | settings_file=None, 75 | local_settings_file=None, 76 | default_settings=None, 77 | start=True, 78 | dispatcher_cls=None, 79 | dispatcher_kwargs=None, 80 | reuse=True, 81 | verbose=False): 82 | """ 83 | Make an application object 84 | """ 85 | # is reuse, then create application only one 86 | if reuse and hasattr(simpleframe.__global__, 'application') and \ 87 | simpleframe.__global__.application: 88 | return simpleframe.__global__.application 89 | 90 | # process settings and local_settings 91 | settings_file = settings_file or os.environ.get('SETTINGS', 'settings.ini') 92 | local_settings_file = local_settings_file or os.environ.get( 93 | 'LOCAL_SETTINGS', 'local_settings.ini') 94 | 95 | dispatcher_cls = dispatcher_cls or simpleframe.Dispatcher 96 | dispatcher_kwargs = dispatcher_kwargs or {} 97 | 98 | services_dir = os.path.normpath(services_dir) 99 | 100 | if services_dir not in sys.path: 101 | sys.path.insert(0, services_dir) 102 | 103 | install_config(services_dir) 104 | 105 | application = dispatcher_cls(services_dir=services_dir, 106 | settings_file=settings_file, 107 | local_settings_file=local_settings_file, 108 | start=start, 109 | default_settings=default_settings, 110 | **dispatcher_kwargs) 111 | 112 | if verbose: 113 | log.info(' * settings file is "%s"' % settings_file) 114 | log.info(' * local settings file is "%s"' % local_settings_file) 115 | 116 | return application 117 | 118 | 119 | def make_simple_application(services_dir='services', 120 | settings_file=None, 121 | local_settings_file=None, 122 | default_settings=None, 123 | start=True, 124 | dispatcher_cls=None, 125 | dispatcher_kwargs=None, 126 | reuse=True, 127 | ): 128 | 129 | return make_application(services_dir=services_dir, 130 | settings_file=settings_file, 131 | local_settings_file=local_settings_file, 132 | start=False, 133 | default_settings=default_settings, 134 | dispatcher_cls=dispatcher_cls, 135 | dispatcher_kwargs=dispatcher_kwargs, 136 | reuse=reuse) 137 | 138 | 139 | class MakeProjectCommand(Command): 140 | name = 'makeproject' 141 | help = 'Create a new project directory according the project name' 142 | args = 'project_name' 143 | check_services_dirs = False 144 | 145 | def handle(self, options, global_options, *args): 146 | from servos.utils.common import extract_dirs 147 | 148 | if not args: 149 | project_name = '' 150 | while not project_name: 151 | project_name = raw_input('Please enter project name:') 152 | else: 153 | project_name = args[0] 154 | 155 | ans = '-1' 156 | if os.path.exists(project_name): 157 | if global_options.yes: 158 | ans = 'y' 159 | while ans not in ('y', 'n'): 160 | ans = raw_input( 161 | 'The project directory has been existed, do you want to overwrite it?(y/n)[n]') 162 | if not ans: 163 | ans = 'n' 164 | else: 165 | ans = 'y' 166 | if ans == 'y': 167 | extract_dirs('servos', 'template_files/project', 168 | project_name, verbose=global_options.verbose) 169 | 170 | # rename .gitignore.template to .gitignore 171 | os.rename(os.path.join(project_name, '.gitignore.template'), os.path.join( 172 | project_name, '.gitignore')) 173 | 174 | register_command(MakeProjectCommand) 175 | 176 | 177 | class MakeServiceCommand(Command): 178 | name = 'makeservice' 179 | args = 'servicename' 180 | help = 'Create a new service according the servicename parameter.' 181 | check_services_dirs = False 182 | 183 | def handle(self, options, global_options, *args): 184 | from servos.utils.common import extract_dirs 185 | 186 | if not args: 187 | service_name = '' 188 | while not service_name: 189 | service_name = raw_input('Please enter service name:') 190 | services = [service_name] 191 | else: 192 | services = args 193 | 194 | for service_name in services: 195 | ans = '-1' 196 | service_path = service_name.replace('.', '//') 197 | if os.path.exists('services'): 198 | path = os.path.join('services', service_path) 199 | else: 200 | path = service_path 201 | 202 | if os.path.exists(path): 203 | if global_options.yes: 204 | ans = 'y' 205 | while ans not in ('y', 'n'): 206 | ans = raw_input( 207 | 'The service directory has been existed, do you want to overwrite it?(y/n)[n]') 208 | if not ans: 209 | ans = 'n' 210 | else: 211 | ans = 'y' 212 | if ans == 'y': 213 | extract_dirs( 214 | 'servos', 'template_files/service', path, verbose=global_options.verbose) 215 | 216 | register_command(MakeServiceCommand) 217 | 218 | 219 | class MakeCmdCommand(Command): 220 | name = 'makecmd' 221 | help = 'Created a commands.py to the services or current directory.' 222 | args = '[servicename, servicename, ...]' 223 | check_services = False 224 | check_services_dirs = False 225 | 226 | def handle(self, options, global_options, *args): 227 | from servos.utils.common import extract_dirs 228 | from servos import get_service_dir 229 | 230 | if not args: 231 | extract_dirs( 232 | 'servos', 'template_files/command', '.', verbose=global_options.verbose) 233 | else: 234 | for f in args: 235 | p = get_service_dir(f) 236 | extract_dirs( 237 | 'servos', 'template_files/command', p, verbose=global_options.verbose) 238 | 239 | register_command(MakeCmdCommand) 240 | 241 | 242 | class InstallCommand(Command): 243 | name = 'install' 244 | help = 'install [servicename,...] extra modules listed in requirements.txt' 245 | args = '[servicename]' 246 | option_list = ( 247 | make_option('-r', '--requirement', dest='requirement', 248 | default='requirements.txt', 249 | help='Global requirements file, default is "%default".'), 250 | ) 251 | 252 | def handle(self, options, global_options, *args): 253 | from servos.core.simpleframe import get_service_dir 254 | 255 | # check pip or setuptools 256 | try: 257 | import pip 258 | except: 259 | print "Error: can't import pip module, please install it first" 260 | sys.exit(1) 261 | 262 | services = args or self.get_services(global_options) 263 | 264 | def get_requirements(): 265 | for service in services: 266 | path = get_service_dir(service) 267 | r_file = os.path.join(path, 'requirements.txt') 268 | if os.path.exists(r_file): 269 | yield r_file 270 | r_file = os.path.join( 271 | global_options.services_dir, options.requirement) 272 | if os.path.exists(r_file): 273 | yield r_file 274 | 275 | for r_file in get_requirements(): 276 | if global_options.verbose: 277 | print "Processing... %s" % r_file 278 | os.system('pip install -r %s' % r_file) 279 | 280 | register_command(InstallCommand) 281 | 282 | 283 | class FindCommand(Command): 284 | name = 'find' 285 | help = 'Find objects in servos, such as: config option etc.' 286 | args = '' 287 | check_services_dirs = True 288 | option_list = ( 289 | make_option('-o', '--option', dest='option', 290 | help='Find ini option defined in which settings.ini.'), 291 | ) 292 | 293 | def handle(self, options, global_options, *args): 294 | # self.get_application(global_options) 295 | if options.option: 296 | self._find_option(global_options, options.option) 297 | else: 298 | self._find_option(global_options, args[0]) 299 | 300 | def _find_option(self, global_options, option): 301 | self.get_application(global_options) 302 | from servos import settings 303 | from servos.core.simpleframe import collect_settings 304 | from servos.utils.pyini import Ini 305 | 306 | print '------ Combined value of [%s] ------' % option 307 | print settings.get_var(option) 308 | 309 | print '------ Detail value of [%s] ------' % option 310 | sec_flag = '/' not in option 311 | if not sec_flag: 312 | section, key = option.split('/') 313 | 314 | for f in collect_settings( 315 | global_options.services_dir, 316 | settings_file=global_options.settings, 317 | local_settings_file=global_options.local_settings): 318 | x = Ini(f, raw=True) 319 | if sec_flag: 320 | if option in x: 321 | print x[option] 322 | else: 323 | if section in x: 324 | if key in x[section]: 325 | v = x[section][key] 326 | print "%s %s%s" % (str(v), key, v.value()) 327 | 328 | register_command(FindCommand) 329 | 330 | 331 | class RunserverCommand(Command): 332 | name = 'runserver' 333 | help = 'Start a new development server.' 334 | args = '' 335 | option_list = ( 336 | make_option('-b', '--background', dest='background', action='store_true', default=False, 337 | help='If run in background. Default is False.'), 338 | ) 339 | 340 | def handle(self, options, global_options, *args): 341 | 342 | if options.background: 343 | daemonize() 344 | 345 | make_application(services_dir=global_options.services_dir, 346 | settings_file=global_options.settings, 347 | local_settings_file=global_options.local_settings, 348 | verbose=global_options.verbose) 349 | 350 | register_command(RunserverCommand) 351 | 352 | 353 | def daemonize(): 354 | """ 355 | do the UNIX double-fork magic, see Stevens' "Advanced 356 | Programming in the UNIX Environment" for details (ISBN 0201563177) 357 | http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 358 | """ 359 | try: 360 | pid = os.fork() 361 | if pid > 0: 362 | # exit first parent 363 | sys.exit(0) 364 | except OSError, e: 365 | sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) 366 | sys.exit(1) 367 | 368 | os.setsid() 369 | 370 | # do second fork 371 | try: 372 | pid = os.fork() 373 | if pid > 0: 374 | # exit from second parent 375 | sys.exit(0) 376 | except OSError, e: 377 | sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) 378 | sys.exit(1) 379 | 380 | # redirect standard file descriptors 381 | sys.stdout.flush() 382 | sys.stderr.flush() 383 | si = file("/dev/null", 'r') 384 | so = file("/dev/null", 'a+') 385 | se = file("/dev/null", 'a+', 0) 386 | os.dup2(si.fileno(), sys.stdin.fileno()) 387 | os.dup2(so.fileno(), sys.stdout.fileno()) 388 | os.dup2(se.fileno(), sys.stderr.fileno()) 389 | 390 | 391 | def call(args=None): 392 | from servos.core.commands import execute_command_line 393 | 394 | def callback(global_options): 395 | services_dir = global_options.services_dir or os.path.join( 396 | os.getcwd(), 'services') 397 | if os.path.exists(services_dir) and services_dir not in sys.path: 398 | sys.path.insert(0, services_dir) 399 | 400 | install_config(services_dir) 401 | 402 | if isinstance(args, (unicode, str)): 403 | import shlex 404 | args = shlex.split(args) 405 | 406 | execute_command_line(args or sys.argv, get_commands, 'servos', callback) 407 | 408 | 409 | def main(): 410 | call() 411 | 412 | if __name__ == '__main__': 413 | main() 414 | -------------------------------------------------------------------------------- /servos/template_files/command/commands.py: -------------------------------------------------------------------------------- 1 | from servos.core.commands import Command 2 | from optparse import make_option 3 | 4 | 5 | class DemoCommand(Command): 6 | name = 'demo' 7 | option_list = ( 8 | make_option('-d', '--demo', dest='demo', default=False, action='store_true', 9 | help='Demo command demo.'), 10 | ) 11 | help = '' 12 | args = '' 13 | check_services_dirs = True 14 | check_services = False 15 | 16 | def handle(self, options, global_options, *args): 17 | print 'This is a demo of DemoCommand, you can enter: ' 18 | print 19 | print ' servos help demo' 20 | print 21 | print 'to test this command' 22 | print 23 | print 'options=', options 24 | print 'global_options=', global_options 25 | print 'args=', args 26 | -------------------------------------------------------------------------------- /servos/template_files/project/.gitignore.template: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.bak 3 | local_settings.ini -------------------------------------------------------------------------------- /servos/template_files/project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | from servos.manage import call 5 | 6 | 7 | if __name__ == '__main__': 8 | call() 9 | -------------------------------------------------------------------------------- /servos/template_files/project/services/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1' -------------------------------------------------------------------------------- /servos/template_files/project/services/local_settings.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | DEBUG = True -------------------------------------------------------------------------------- /servos/template_files/project/services/settings.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | DEBUG = False 3 | 4 | #INSTALLED_APPS = [ 5 | # ] -------------------------------------------------------------------------------- /servos/template_files/project/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | setup( 5 | setup_requires=['nose>=1.0', 'pbr>=1.8'], 6 | test_suite='nose.collector', 7 | pbr=True 8 | ) 9 | -------------------------------------------------------------------------------- /servos/template_files/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knitmesh/servos-framework/45fdc04580890c3aac039c023f104ce8dc00af08/servos/template_files/service/__init__.py -------------------------------------------------------------------------------- /servos/template_files/service/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is here because many Platforms as a Service look for 2 | # requirements.txt in the root directory of a project. 3 | -------------------------------------------------------------------------------- /servos/template_files/service/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | 4 | from servos import ServiceBase, entry, settings 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | @entry() 9 | class DemoService(ServiceBase): 10 | _service_name = 'demoservice' 11 | 12 | def start(self): 13 | logger.info('starting %s service' % self._service_name) 14 | super(DemoService, self).start() 15 | logger.info('%s service started' % self._service_name) 16 | 17 | def stop(self): 18 | logger.info('stoping %s service' % self._service_name) 19 | super(DemoService, self).stop() 20 | logger.info('%s service stoped' % self._service_name) 21 | -------------------------------------------------------------------------------- /servos/template_files/service/settings.ini: -------------------------------------------------------------------------------- 1 | [DEPENDS] 2 | REQUIRED_SERVICES=[] 3 | 4 | [BINDS] 5 | -------------------------------------------------------------------------------- /servos/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knitmesh/servos-framework/45fdc04580890c3aac039c023f104ce8dc00af08/servos/utils/__init__.py -------------------------------------------------------------------------------- /servos/utils/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import logging 4 | import inspect 5 | import sys 6 | 7 | log = logging 8 | 9 | 10 | def safe_import(path): 11 | module = path.split('.') 12 | g = __import__(module[0], fromlist=['*']) 13 | s = [module[0]] 14 | for i in module[1:]: 15 | mod = g 16 | if hasattr(mod, i): 17 | g = getattr(mod, i) 18 | else: 19 | s.append(i) 20 | g = __import__('.'.join(s), fromlist=['*']) 21 | return mod, g 22 | 23 | 24 | def import_mod_attr(path): 25 | """ 26 | Import string format module, e.g. 'servos.utils.sorteddict' or an object 27 | return module object and object 28 | """ 29 | if isinstance(path, (str, unicode)): 30 | v = path.split(':') 31 | if len(v) == 1: 32 | module, func = path.rsplit('.', 1) 33 | else: 34 | module, func = v 35 | mod = __import__(module, fromlist=['*']) 36 | f = mod 37 | for x in func.split('.'): 38 | f = getattr(f, x) 39 | else: 40 | f = path 41 | mod = inspect.getmodule(path) 42 | return mod, f 43 | 44 | 45 | def import_attr(func): 46 | mod, f = import_mod_attr(func) 47 | return f 48 | 49 | 50 | def myimport(module): 51 | mod = __import__(module, fromlist=['*']) 52 | return mod 53 | 54 | 55 | class MyPkg(object): 56 | 57 | @staticmethod 58 | def resource_filename(module, path): 59 | mod = myimport(module) 60 | p = os.path.dirname(mod.__file__) 61 | if path: 62 | return os.path.join(p, path) 63 | else: 64 | return p 65 | 66 | @staticmethod 67 | def resource_listdir(module, path): 68 | d = MyPkg.resource_filename(module, path) 69 | return os.listdir(d) 70 | 71 | @staticmethod 72 | def resource_isdir(module, path): 73 | d = MyPkg.resource_filename(module, path) 74 | return os.path.isdir(d) 75 | 76 | try: 77 | import pkg_resources as pkg 78 | except: 79 | pkg = MyPkg 80 | 81 | 82 | def norm_path(path): 83 | return os.path.normpath(os.path.abspath(path)) 84 | 85 | r_expand_path = re.compile('\$\[(\w+)\]') 86 | 87 | 88 | def expand_path(path): 89 | """ 90 | Auto search some variables defined in path string, such as: 91 | $[PROJECT]/files 92 | $[service_name]/files 93 | for $[PROJECT] will be replaced with servos application services_dir directory 94 | and others will be treated as a normal python package, so servos will 95 | use pkg_resources to get the path of the package 96 | 97 | """ 98 | from servos import application 99 | 100 | def replace(m): 101 | txt = m.groups()[0] 102 | if txt == 'PROJECT': 103 | return application.services_dir 104 | else: 105 | return pkg.resource_filename(txt, '') 106 | return re.sub(r_expand_path, replace, path) 107 | 108 | 109 | def check_services_dir(services_dir): 110 | if not os.path.exists(services_dir): 111 | print >>sys.stderr, "[Error] Can't find the services_dir [%s], please check it out" % services_dir 112 | sys.exit(1) 113 | 114 | 115 | def is_pyfile_exist(dir, pymodule): 116 | path = os.path.join(dir, '%s.py' % pymodule) 117 | if not os.path.exists(path): 118 | path = os.path.join(dir, '%s.pyc' % pymodule) 119 | if not os.path.exists(path): 120 | path = os.path.join(dir, '%s.pyo' % pymodule) 121 | if not os.path.exists(path): 122 | return False 123 | return True 124 | 125 | 126 | def extract_file(module, path, dist, verbose=False, replace=True): 127 | outf = os.path.join(dist, os.path.basename(path)) 128 | import shutil 129 | 130 | inf = pkg.resource_filename(module, path) 131 | sfile = os.path.basename(inf) 132 | if os.path.isdir(dist): 133 | dfile = os.path.join(dist, sfile) 134 | else: 135 | dfile = dist 136 | f = os.path.exists(dfile) 137 | if replace or not f: 138 | shutil.copy2(inf, dfile) 139 | if verbose: 140 | print 'Copy %s to %s' % (inf, dfile) 141 | 142 | 143 | def extract_dirs(mod, path, dst, verbose=False, exclude=None, exclude_ext=None, recursion=True, replace=True): 144 | """ 145 | mod name 146 | path mod path 147 | dst output directory 148 | resursion True will extract all sub module of mod 149 | """ 150 | default_exclude = ['.svn', '_svn', '.git'] 151 | default_exclude_ext = ['.pyc', '.pyo', '.bak', '.tmp'] 152 | exclude = exclude or [] 153 | exclude_ext = exclude_ext or [] 154 | if not os.path.exists(dst): 155 | os.makedirs(dst) 156 | if verbose: 157 | print 'Make directory %s' % dst 158 | for r in pkg.resource_listdir(mod, path): 159 | if r in exclude or r in default_exclude: 160 | continue 161 | fpath = os.path.join(path, r) 162 | if pkg.resource_isdir(mod, fpath): 163 | if recursion: 164 | extract_dirs(mod, fpath, os.path.join( 165 | dst, r), verbose, exclude, exclude_ext, recursion, replace) 166 | else: 167 | ext = os.path.splitext(fpath)[1] 168 | if ext in exclude_ext or ext in default_exclude_ext: 169 | continue 170 | extract_file(mod, fpath, dst, verbose, replace) 171 | -------------------------------------------------------------------------------- /servos/utils/localproxy.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | # inspired from http://code.activestate.com/recipes/496741-object-proxying/ 4 | 5 | 6 | class Global(object): 7 | pass 8 | 9 | 10 | class LocalProxy(object): 11 | __slots__ = ['_env', '_obj_name'] 12 | 13 | def __init__(self, environ, name, klass): 14 | object.__setattr__(self, "_env", environ) 15 | object.__setattr__(self, "_obj_name", name) 16 | 17 | def __get_instance__(self): 18 | return getattr(self._env, self._obj_name, None) 19 | 20 | def __getattr__(self, name): 21 | return getattr(self.__get_instance__(), name) 22 | 23 | def __setattr__(self, name, value): 24 | setattr(self.__get_instance__(), name, value) 25 | 26 | def __delattr__(self, name): 27 | delattr(self.__get_instance__(), name) 28 | 29 | def __nonzero__(self): 30 | return bool(self.__get_instance__()) 31 | 32 | def __str__(self): 33 | return str(self.__get_instance__()) 34 | 35 | def __repr__(self): 36 | return repr(self.__get_instance__()) 37 | 38 | # 39 | # factories 40 | # 41 | _special_names = [ 42 | '__abs__', '__add__', '__and__', '__call__', '__cmp__', '__coerce__', 43 | '__contains__', '__delitem__', '__delslice__', '__div__', '__divmod__', 44 | '__eq__', '__float__', '__floordiv__', '__ge__', '__getitem__', 45 | '__getslice__', '__gt__', '__hash__', '__hex__', '__iadd__', '__iand__', 46 | '__idiv__', '__idivmod__', '__ifloordiv__', '__ilshift__', '__imod__', 47 | '__imul__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', 48 | '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', 49 | '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', 50 | '__neg__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', 51 | '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', 52 | '__repr__', '__reversed__', '__rfloorfiv__', '__rlshift__', '__rmod__', 53 | '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', 54 | '__rtruediv__', '__rxor__', '__setitem__', '__setslice__', '__sub__', 55 | '__truediv__', '__xor__', 'next', 56 | ] 57 | 58 | @classmethod 59 | def _create_class_proxy(cls, theclass): 60 | """creates a proxy for the given class""" 61 | 62 | def make_method(name): 63 | def method(self, *args, **kw): 64 | return getattr(self.__get_instance__(), name)(*args, **kw) 65 | return method 66 | 67 | namespace = {} 68 | for name in cls._special_names: 69 | if hasattr(theclass, name): 70 | namespace[name] = make_method(name) 71 | return type("%s(%s)" % (cls.__name__, theclass.__name__), (cls,), namespace) 72 | 73 | def __new__(cls, env, name, klass, *args, **kwargs): 74 | """ 75 | creates an proxy instance referencing `obj`. (obj, *args, **kwargs) are 76 | passed to this class' __init__, so deriving classes can define an 77 | __init__ method of their own. 78 | note: _class_proxy_cache is unique per deriving class (each deriving 79 | class must hold its own cache) 80 | """ 81 | try: 82 | cache = cls.__dict__["_class_proxy_cache"] 83 | except KeyError: 84 | cls._class_proxy_cache = cache = {} 85 | try: 86 | theclass = cache[klass] 87 | except KeyError: 88 | cache[klass] = theclass = cls._create_class_proxy(klass) 89 | ins = object.__new__(theclass) 90 | return ins 91 | -------------------------------------------------------------------------------- /servos/utils/pyini.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # coding=utf-8 3 | # This module is used to parse and create new style ini file. This 4 | # new style ini file format should like a very simple python program 5 | # but you can define section like normal ini file, for example: 6 | # 7 | # [default] 8 | # key = value 9 | # 10 | # Then the result should be ini.default.key1 = value1 11 | # Whole ini file will be parsed into a dict-like object, but you can access 12 | # each section and key with '.', just like ini.section.key 13 | # For value can be simple python data type, such as: tuple, list, dict, int 14 | # float, string, etc. So it's very like a normal python file, but it's has 15 | # some sections definition. 16 | 17 | import sys 18 | import os 19 | import re 20 | import codecs 21 | import StringIO 22 | import locale 23 | import copy 24 | import tokenize 25 | import token 26 | from sorteddict import SortedDict 27 | from traceback import print_exc 28 | 29 | __all__ = ['SortedDict', 'Section', 'Ini', 'uni_prt'] 30 | 31 | try: 32 | set 33 | except: 34 | from sets import Set as set 35 | 36 | try: 37 | defaultencoding = locale.getdefaultlocale()[1] 38 | except: 39 | defaultencoding = None 40 | 41 | if not defaultencoding: 42 | defaultencoding = 'UTF-8' 43 | try: 44 | codecs.lookup(defaultencoding) 45 | except: 46 | defaultencoding = 'UTF-8' 47 | 48 | r_encoding = re.compile(r'\s*coding\s*[=:]\s*([-\w.]+)') 49 | r_var = re.compile(ur'(? 1: 267 | self.cached_value = self.eval(self.values[-1]) 268 | break 269 | else: 270 | result.append(value) 271 | 272 | if result: 273 | self.cached_value = merge_data(result) 274 | 275 | # sync 276 | self.globals[self.sec_name][self.key] = self.cached_value 277 | 278 | return self.cached_value 279 | 280 | 281 | class RawValue(object): 282 | 283 | def __init__(self, filename, lineno, text, replace_flag=''): 284 | self.filename = filename 285 | self.lineno = lineno 286 | self.text = text 287 | self.replace_flag = replace_flag 288 | 289 | def __str__(self): 290 | _length = 26 291 | if len(self.filename) > _length: 292 | s = self.filename.replace('\\', '/').split('/') 293 | t = -1 294 | for i in range(len(s) - 1, -1, -1): 295 | t = len(s[i]) + t + 1 296 | if t > _length: 297 | break 298 | filename = '.../' + '/'.join(s[i + 1:]) 299 | else: 300 | filename = self.filename.replace('\\', '/') 301 | return '%-30s:%04d' % (filename, self.lineno) 302 | 303 | def value(self): 304 | if self.replace_flag: 305 | op = ' <= ' 306 | else: 307 | op = ' = ' 308 | return "%s%s" % (op, self.text) 309 | 310 | 311 | class Section(SortedDict): 312 | 313 | def __init__(self, name, comments=None, encoding=None, root=None, info=None): 314 | super(Section, self).__init__() 315 | self._root = root 316 | self._name = name 317 | self.add_comment(comments=comments) 318 | self._field_comments = {} 319 | self._field_flag = {} 320 | self._encoding = encoding 321 | self._info = info 322 | 323 | # sync 324 | if self._root and self._lazy: 325 | # self._root._globals.setdefault(name, SortedDict()) 326 | self._root._globals[name] = SortedDict() 327 | 328 | @property 329 | def _lazy(self): 330 | if self._root: 331 | return self._root._lazy 332 | else: 333 | return False 334 | 335 | def add(self, key, value, comments=None, replace=False): 336 | self.__setitem__(key, value, replace) 337 | self._field_flag[key] = replace 338 | self.add_comment(key, comments) 339 | 340 | def __setitem__(self, key, value, replace=False): 341 | if self._lazy: 342 | if key not in self or replace: 343 | v = Lazy(key, self._root._globals, self._name, 344 | self._encoding, self._root._import_env) 345 | else: 346 | v = self[key] 347 | v.add(value) 348 | else: 349 | v = value 350 | if not replace: 351 | v = merge_data([value], self.get(key)) 352 | 353 | super(Section, self).__setitem__(key, v, append=replace) 354 | 355 | def add_comment(self, key=None, comments=None): 356 | comments = comments or [] 357 | if not isinstance(comments, (tuple, list)): 358 | comments = [comments] 359 | if not key: 360 | self._comments = comments 361 | else: 362 | self._field_comments[key] = copy.copy(comments) 363 | 364 | def comment(self, key=None): 365 | if not key: 366 | return self._comments 367 | else: 368 | return self._field_comments.get(key, []) 369 | 370 | def del_comment(self, key=None): 371 | self.add_comment(key, None) 372 | 373 | def dumps(self, out, convertors=None): 374 | if self._comments: 375 | print >> out, '\n'.join(self._comments) 376 | if self._root and self._root._raw: 377 | print >> out, '%s [%s]' % (self._info, self._name) 378 | else: 379 | print >> out, '[%s]' % self._name 380 | for f in self.keys(): 381 | comments = self.comment(f) 382 | if comments: 383 | print >> out, '\n'.join(comments) 384 | if self._root and self._root._raw: 385 | print >> out, "%s %s%s" % (str(self[f]), f, self[f].value()) 386 | else: 387 | if self._field_flag.get(f, False): 388 | op = ' <= ' 389 | else: 390 | op = ' = ' 391 | buf = f + op + \ 392 | uni_prt(self[f], self._encoding, convertors=convertors) 393 | if len(buf) > 79: 394 | buf = f + op + \ 395 | uni_prt( 396 | self[f], self._encoding, True, convertors=convertors) 397 | print >> out, buf 398 | 399 | def __delitem__(self, key): 400 | super(Section, self).__delitem__(key) 401 | self.del_comment(key) 402 | 403 | def __delattr__(self, key): 404 | try: 405 | del self[key] 406 | except KeyError: 407 | raise 408 | 409 | def __str__(self): 410 | buf = StringIO.StringIO() 411 | self.dumps(buf) 412 | return buf.getvalue() 413 | 414 | 415 | class Ini(SortedDict): 416 | 417 | def __init__(self, inifile='', commentchar=None, encoding=None, 418 | env=None, convertors=None, lazy=False, writable=False, raw=False, 419 | import_env=True): 420 | """ 421 | lazy is used to parse first but not deal at time, and only when 422 | the user invoke finish() function, it'll parse the data. 423 | 424 | import_env will import all environment variables 425 | """ 426 | super(Ini, self).__init__() 427 | self._inifile = inifile 428 | self._commentchar = commentchar or __default_env__.get( 429 | 'commentchar', '#') 430 | self._encoding = encoding or __default_env__.get('encoding', 'utf-8') 431 | self._env = __default_env__.get('env', {}).copy() 432 | self._env.update(env or {}) 433 | self._env['set'] = set 434 | self.update(self._env) 435 | self._globals = SortedDict() 436 | self._import_env = import_env 437 | if self._import_env: 438 | self._globals.update(os.environ) 439 | 440 | self._convertors = __default_env__.get('convertors', {}).copy() 441 | self._convertors.update(convertors or {}) 442 | self._lazy = lazy 443 | self._writable = writable 444 | self._raw = raw 445 | 446 | if lazy: 447 | self._globals.update(self._env.copy()) 448 | 449 | if self._inifile: 450 | self.read(self._inifile) 451 | 452 | def set_filename(self, filename): 453 | self._inifile = filename 454 | 455 | def get_filename(self): 456 | return self._inifile 457 | 458 | filename = property(get_filename, set_filename) 459 | 460 | def read(self, fobj, filename=''): 461 | encoding = None 462 | 463 | if isinstance(fobj, (str, unicode)): 464 | f = open(fobj, 'rb') 465 | text = f.read() 466 | f.close() 467 | else: 468 | text = fobj.read() 469 | 470 | text = text + '\n' 471 | begin = 0 472 | if text.startswith(codecs.BOM_UTF8): 473 | begin = 3 474 | encoding = 'UTF-8' 475 | elif text.startswith(codecs.BOM_UTF16): 476 | begin = 2 477 | encoding = 'UTF-16' 478 | 479 | if not encoding: 480 | try: 481 | unicode(text, 'UTF-8') 482 | encoding = 'UTF-8' 483 | except: 484 | encoding = defaultencoding 485 | 486 | self._encoding = encoding 487 | 488 | f = StringIO.StringIO(text) 489 | f.seek(begin) 490 | lineno = 0 491 | comments = [] 492 | section = None 493 | while 1: 494 | lastpos = f.tell() 495 | line = f.readline() 496 | lineno += 1 497 | if not line: 498 | break 499 | line = line.strip() 500 | if line: 501 | if line.startswith(self._commentchar): 502 | if lineno == 1: # first comment line 503 | b = r_encoding.search(line[1:]) 504 | if b: 505 | self._encoding = b.groups()[0] 506 | continue 507 | comments.append(line) 508 | elif line.startswith('[') and line.endswith(']'): 509 | sec_name = line[1:-1].strip() 510 | # process include notation 511 | if sec_name.startswith('include:'): 512 | _filename = sec_name[8:].strip() 513 | _filename = os.path.abspath(_filename) 514 | if os.path.exists(_filename): 515 | old_encoding = self._encoding 516 | self.read(_filename) 517 | self._encoding = old_encoding 518 | else: 519 | import warnings 520 | warnings.warn( 521 | Warning("Can't find the file [%s], so just skip it" % _filename), stacklevel=2) 522 | continue 523 | info = RawValue(self._inifile, lineno, sec_name) 524 | section = self.add(sec_name, comments, info=info) 525 | comments = [] 526 | elif '=' in line: 527 | if section is None: 528 | raise Exception( 529 | "No section found, please define it first in %s file" % self.filename) 530 | 531 | # if find <=, then it'll replace the old value for mutable variables 532 | # because the default behavior will merge list and dict 533 | pos = line.find('<=') 534 | if pos != -1: 535 | begin, end = pos, pos + 2 536 | replace_flag = True 537 | else: 538 | pos = line.find('=') 539 | begin, end = pos, pos + 1 540 | replace_flag = False 541 | 542 | keyname = line[:begin].strip() 543 | # check keyname 544 | if keyname in self._env: 545 | raise KeyError( 546 | "Settings key %s is alread defined in env, please change it's name" % keyname) 547 | 548 | rest = line[end:].strip() 549 | # if key= then value will be set '' 550 | if rest == '': 551 | value = 'None' 552 | else: 553 | f.seek(lastpos + end) 554 | try: 555 | value, iden_existed, _lineno = self.__read_line(f) 556 | # 因为上面seek到上次的位置了,要减去 557 | _lineno -= 1 558 | except Exception: 559 | print_exc() 560 | raise Exception("Parsing ini file error in %s:%d:%s" % ( 561 | filename or self._inifile, lineno, line)) 562 | if self._lazy: 563 | if iden_existed: 564 | v = EvalValue( 565 | value, filename or self._inifile, lineno, line) 566 | else: 567 | v = value 568 | else: 569 | if self._raw: 570 | v = RawValue( 571 | self._inifile, lineno, value, replace_flag) 572 | else: 573 | try: 574 | v = eval_value( 575 | value, self.env(), self[sec_name], self._encoding, self._import_env) 576 | except Exception: 577 | print_exc() 578 | print dict(self) 579 | raise Exception("Converting value (%s) error in %s:%d:%s" % ( 580 | value, filename or self._inifile, lineno, line)) 581 | 582 | # 修正行数 583 | lineno += _lineno 584 | 585 | section.add(keyname, v, comments, replace=replace_flag) 586 | comments = [] 587 | else: 588 | comments.append(line) 589 | 590 | def save(self, filename=None): 591 | if not filename: 592 | filename = self.filename 593 | if not filename: 594 | filename = sys.stdout 595 | if isinstance(filename, (str, unicode)): 596 | f = open(filename, 'wb') 597 | need_close = True 598 | else: 599 | f = filename 600 | need_close = False 601 | 602 | print >> f, '#coding=%s' % self._encoding 603 | for s in self.keys(): 604 | if s in self._env: 605 | continue 606 | section = self[s] 607 | section.dumps(f, convertors=self._convertors) 608 | 609 | if need_close: 610 | f.close() 611 | 612 | def __read_line(self, f): 613 | """ 614 | Get logic line according the syntax not the physical line 615 | It'll return the line text and if there is identifier existed 616 | 617 | return line, bool 618 | """ 619 | g = tokenize.generate_tokens(f.readline) 620 | 621 | buf = [] 622 | time = 0 623 | iden_existed = False 624 | while 1: 625 | v = g.next() 626 | tokentype, t, (lineno, start), end, line = v 627 | if tokentype == 54: 628 | continue 629 | if tokentype in (token.INDENT, token.DEDENT, tokenize.COMMENT): 630 | continue 631 | if tokentype == token.NAME: 632 | iden_existed = True 633 | if tokentype == token.NEWLINE: 634 | return ''.join(buf), iden_existed, lineno 635 | else: 636 | if t == '=' and time == 0: 637 | time += 1 638 | continue 639 | buf.append(t) 640 | 641 | def __setitem__(self, key, value): 642 | if key not in self: 643 | super(Ini, self).__setitem__(key, value) 644 | 645 | def update(self, value): 646 | for k, v in value.items(): 647 | self.set_var(k, v) 648 | 649 | def add(self, sec_name, comments=None, info=None): 650 | if sec_name in self: 651 | section = self[sec_name] 652 | else: 653 | section = Section( 654 | sec_name, comments, self._encoding, root=self, info=info) 655 | self[sec_name] = section 656 | return section 657 | 658 | def __str__(self): 659 | buf = StringIO.StringIO() 660 | self.save(buf) 661 | return buf.getvalue() 662 | 663 | def get_var(self, key, default=None): 664 | obj = self 665 | for i in key.split('/', 1): 666 | obj = obj.get(i) 667 | if obj is None: 668 | break 669 | 670 | if obj is None: 671 | return default 672 | return obj 673 | 674 | def set_var(self, key, value): 675 | s = key.split('/', 1) 676 | obj = self 677 | for i in s[:-1]: 678 | obj = obj.add(i) 679 | obj[s[-1]] = value 680 | 681 | return True 682 | 683 | def del_var(self, key): 684 | s = key.split('/', 1) 685 | obj = self 686 | for i in s[:-1]: 687 | obj = obj.get(i) 688 | if obj is None: 689 | return False 690 | 691 | if s[-1] in obj: 692 | del obj[s[-1]] 693 | flag = True 694 | else: 695 | flag = False 696 | 697 | return flag 698 | 699 | def items(self): 700 | return ((k, self[k]) for k in self.keys() if k not in self._env) 701 | 702 | def env(self): 703 | if self._import_env: 704 | d = {} 705 | d.update(os.environ.copy()) 706 | d.update(dict(self)) 707 | return d 708 | return self 709 | 710 | def freeze(self): 711 | """ 712 | Process all EvalValue to real value 713 | """ 714 | self._lazy = False 715 | for k, v in self.items(): 716 | if k in self._env: 717 | continue 718 | for _k, _v in v.items(): 719 | if isinstance(_v, Lazy): 720 | if self.writable: 721 | _v.get() 722 | else: 723 | v.__setitem__(_k, _v.get(), replace=True) 724 | del _v 725 | del self._globals 726 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = servos-framework 3 | description-file = 4 | README.md 5 | classifier = 6 | Operating System :: OS Independent 7 | Programming Language :: Python 8 | Programming Language :: Python :: 2.7 9 | 10 | [files] 11 | packages = 12 | servos 13 | 14 | [pbr] 15 | warnerrors = true 16 | 17 | [egg_info] 18 | tag_build = 19 | tag_date = 0 20 | tag_svn_revision = 0 21 | 22 | [entry_points] 23 | console_scripts = 24 | servos = servos.manage:main 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | setup( 5 | setup_requires=['nose>=1.0', 'pbr>=1.8'], 6 | test_suite='nose.collector', 7 | package_data={ 8 | '': ['locale/en/LC_MESSAGES/*.mo', 'locale/zh_CN/LC_MESSAGES/*.mo'], 9 | }, 10 | pbr=True 11 | ) 12 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # Importing from requirements.txt 2 | -r requirements.txt 3 | nose>=1.0 4 | mock==2.0.0 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.0 3 | #indexserver = 4 | # default = http://mypypi.org 5 | envlist = py27 6 | skipsdist = True 7 | 8 | [testenv] 9 | recreate = False 10 | usedevelop = True 11 | sitepackages = True 12 | whitelist_externals = bash 13 | rm 14 | install_command = pip install -U {opts} {packages} 15 | setenv = VIRTUAL_ENV={envdir} 16 | # Pass $HOME var into .tox venv 17 | HOME={env:HOME:} 18 | 19 | deps = -r{toxinidir}/test-requirements.txt 20 | 21 | 22 | [testenv:py27] 23 | whitelist_externals = 24 | bash 25 | commands = 26 | {envpython} {toxinidir}/setup.py test 27 | 28 | 29 | [testenv:pep8] 30 | basepython = python2.7 31 | deps = flake8 32 | 33 | commands = 34 | flake8 35 | 36 | [flake8] 37 | # E125 is deliberately excluded. See 38 | # https://github.com/jcrocholl/pep8/issues/126. It's just wrong. 39 | # 40 | # Most of the whitespace related rules (E12* and E131) are excluded 41 | # because while they are often useful guidelines, strict adherence to 42 | # them ends up causing some really odd code formatting and forced 43 | # extra line breaks. Updating code to enforce these will be a hard sell. 44 | # 45 | # H405 is another one that is good as a guideline, but sometimes 46 | # multiline doc strings just don't have a natural summary 47 | # line. Rejecting code for this reason is wrong. 48 | # 49 | # E251 Skipped due to https://github.com/jcrocholl/pep8/issues/301 50 | ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,H405,E501 51 | exclude = .venv,.git,.tox,dist,docs,bin,local,requirements,*lib/python*,*egg,build,tools/xenserver*,releasenotes 52 | 53 | max-complexity=35 54 | max_line_length = 140 55 | 56 | --------------------------------------------------------------------------------