Package: blue-krill
Blue krillPython 常用工具包模块。使用指南1. blue_krill.secure加解密、安全相关的工具与模块。1.1 dj_environ 模块dj_environ 的主要目的是在django-environ之上，增加可以阅读加密环境变量的能力。如需使用，首先需要设置密文环境变量：exportDATABASE_URL='<使用 bk-secure 加密后的字符串>'exportFOO='<使用 bk-secure 加密后的字符串>'然后在settings模块中，初始化SecureEnv对象并使用它读取对应配置：fromblue_krill.secure.dj_environimportSecureEnv# 初始化并加载 .env 文件内容sec_env=SecureEnv()environ.Env.read_env()# 读取为数据库配置DATABASES['default']=sec_env.db()# 读取为普通配置FOO=sec_env('FOO')1.2 bk-secure 脚本bk-secure 主要用于配合 dj_environ 模块生成加密环境变量（或配置文件）。使用前，先将BK_FERNET_KEY设为你所使用的加密 key。一般情况下，这个值等同于 Django 项目的BKKRILL_ENCRYPT_SECRET_KEY配置项：export BK_FERNET_KEY='... ...'执行encrypt加密某段明文：❯ bk-secure encryptInput string: mysql://u:p@localhost/fooThe encrypted token is: gAAAAABfKUtKIBzYc_gyQL-j9TmI35O1d0auLQfYeso6D8Q77ZC9PIuv26ABPFlOQSSPDzT3HcVrhI1K3XwU5Xfs6gP6iAe8RhEAJJhMktp7CKzn7p7imNk=执行decrypt解密某段密文：❯ bk-secure decryptInput token: gAAAAABfKUtKIBzYc_gyQL-j9TmI35O1d0auLQfYeso6D8Q77ZC9PIuv26ABPFlOQSSPDzT3HcVrhI1K3XwU5Xfs6gP6iAe8RhEAJJhMktp7CKzn7p7imNk=The decrypted result is: mysql://u:p@localhost/foo使用edit以明文方式编辑某段密文，并输出新的密文：❯ bk-secure editInput token: gAAAAABfKUtKIBzYc_gyQL-j9TmI35O1d0auLQfYeso6D8Q77ZC9PIuv26ABPFlOQSSPDzT3HcVrhI1K3XwU5Xfs6gP6iAe8RhEAJJhMktp7CKzn7p7imNk=#将弹出编辑器界面修改明文，可通过$EDITOR环境变量设置编辑器The new value is: mysql://u:p@localhost/foo2The encrypted new value is: gAAAAABfKUui5_YUVxoYEYQG61RSRX1Ll3s1dgkZ5nUEJbCxakWHSyo3AKZFv3GuoQ7cH2Hm5LEU2QDK8C3G-_iog0TmqSbVkIYf0WnksH2DGgedldfbwhs=2. blue_krill.editions多版本开发相关工具模块2.1 editionctl 工具editonctl是用来帮助开发需要支持多版本 Python 项目的工具，使用该工具前，请先把你的项目目录组织成下面这种结构：├── editionctl.toml
├── editions
│   ├── ee
│   │   └── ee.py
│   └── te
│       └── te.py
└── main
    └── main.py其中：editions：仅保存不同版本所特有的源码文件main：项目主目录2.1.1 创建配置文件要使用editonctl，首先需要在项目内创建配置文件editionctl.toml。比如，针对上面的项目结构，我们可以创建这样的配置文件：# 项目主目录project_root='main'# 项目各版本所在目录editions_root='editions'[[editions]]# 版本名称name="TE"# 版本相对路径rel_directory='te'[[editions]]name="EE"rel_directory='ee'更多配置文件相关说明，可执行editionctl help查看。2.1.2 在版本之间切换在不同版本间切换，需要使用editionctl activate {EDITION_NAME}命令。执行该命令后，工具会将指定版本下的所有源码文件，拷贝到project_root中。$editionctlactivateEE[2020-12-1716:51:37,312]INFO:EditionEEactivated,linkerisdefault为了避免由工具拷贝的文件被意外提交到源码仓库，这些文件会被添加到{project_root}/.gitignore中。2.1.3 重置多版本假如你想要清除所有由editionctl工具创建的源码文件，可以执行editionctl reset命令。执行该命令将删除所有多版本相关文件，只保留主目录。2.1.4 进入开发模式在开发多版本项目时，editions_root目录下的当前版本相关文件会被频繁修改。正常情况下，每次修改版本文件后，我们都要手动重新执行activate命令重新同步文件。为了简化这个过程，我们可以使用editionctl develop。执行editionctl develop命令后，工具将会持续监听当前edition目录下的任何改动。如果监听到新改动，则自动触发同步机制。$ editionctl develop
[2020-12-17 16:56:34,385] INFO: Start watching editions/ee directory for edition EE...3. blue_krill.data_types.enum枚举相关的数据类型。3.1 FeatureFlag功能标记(Feature Flag)用于控制当前用户能否感知到某个功能/特性，只提供**开启(enabled)和关闭(disabled)**两个状态, 分别对应于布尔值的 True/Flase。
为了避免各项目重复造轮子, blue_krill 抽象出通用的 FeatureFlag 模型, 同时也提供类似于枚举(Enum)的API, 降低使用成本。3.1.1 如何定义 FeatureFlag就像定义普通的 Python Class 一样, 定义 FeatureFlag 只需要继承blue_krill.data_types.enum::FeatureFlag即可。fromblue_krill.data_types.enumimportFeatureFlag,FeatureFlagFieldclassUserFeatureFlag(FeatureFlag):# 使用类属性声明 FeatureFlagField 时, name 属性会通过描述符协议自动设置, 无需额外指定.WEBCONSOLE=FeatureFlagField(label="使用 WEBConsole")CREATE_SMART_APP=FeatureFlagField(label="创建 Smart 应用")...3.1.2 如何添加额外的 FeatureFlag不同于枚举值(Enum), FeatureFlag 允许在运行过程中动态添加额外的字段或修改已有字段的默认值。# 添加额外的 FeatureFlag 时, 需要制定对应的名称.UserFeatureFlag.register_feature_flag(FeatureFlagField(name="CHOOSE_SOURCE_ORIGIN",label="选择源码来源"))3.2 StructuredEnum考虑到我们使用枚举值(Enum)时, 往往会给枚举值添加额外的描述字段，为了避免各项目重复造轮子，blue_krill 基于Enum实现了 StructuredEnum，可以基于配套的EnumField定义带有额外描述内容的枚举值。fromblue_krill.data_types.enumimportEnumField,StructuredEnumclassDiffType(str,StructuredEnum):ADDED=EnumField("added",label="新增")DELETED=EnumField("deleted",label="删除")NOT_MODIFIED=EnumField("not_modified",label="未改动")4. blue_krill.storages.blobstore对象存储服务的简单封装, 目前支持上传,下载,生成预签名URL三个接口.4.1 S3StoreS3 协议的 BlobStore 实现, 使用时需要额外安装 boto3=='^1.4.3', 可参考以下代码进行实例化:fromblue_krill.storages.blobstore.s3importS3Storestore=S3Store(bucket="your-bucket",aws_access_key_id='your-access-key',aws_secret_access_key='your-secret-key',endpoint_url='your-s3-endpoint',# Optionalregion_name='your-region, default is `us-east-1`',signature_version='your-signature-version, default is s3v4',)4.2 BKGenericRepo底层服务是蓝鲸通用二进制仓库的底层实现, 可参考以下代码进行实例化:fromblue_krill.storages.blobstore.bkrepoimportBKGenericRepostore=BKGenericRepo(bucket='your-bucket',project='your-project-id',endpoint_url='',)5. blue_krill.webblue_krill.web主要存放与 Web 开发有关的工具集。5.1 blue_krill.web.std_error该模块内包含标准的错误码功能。std_error最常见的用法，是通过ErrorCode定义一套错误码集合：fromblue_krill.web.std_errorimportErrorCodeclassErrorCodes:CREATE_ERROR=ErrorCode('创建失败')DELETE_ERROR=ErrorCode('删除失败')# 实例化一个全局对象error_codes=ErrorCodes()当你要抛出某个特定错误时，可以使用下面的语句：raiseerror_codes.CREATE_ERROR# 使用 .f() / .format() 方法追加错误信息raiseerror_codes.CREATE_ERROR.f('追加说明')# 传递 replace=True 替换错误信息raiseerror_codes.CREATE_ERROR.f('替换信息',replace=True)# 设置异常对象的 data 属性，以便在后续处理。可通过 exc_obj.data 属性读取raiseerror_codes.CREATE_ERROR.set_data({'your':'data'})注意：APIError是不可变类型，调用format()会克隆并返回一个新对象，而非修改现有对象。当程序抛出APIError异常后，为了让用户正常看到错误响应，你必须在 Web 框架里捕获并处理该类异常。5.1.1 在框架内捕获 APIError 异常视 Web 框架的不同，捕获与处理APIError的方式会略有区别，以DRF框架为例。要捕获APIError异常，我们首先得创建一个新函数：# file: my_module.pyfromblue_krill.web.std_errorimportAPIErrordefcustom_exception_handler(exc,context):ifisinstance(exc,APIError):# 你可以随意修改这里的响应数据结构data={'code':exc.code,'detail':exc.message,}returnResponse(data,status=exc.status_code)# ... 其他异常处理撮箕创建完函数后，下一步是修改项目配置，将EXCEPTION_HANDLER调整为该异常捕获函数：REST_FRAMEWORK={'EXCEPTION_HANDLER':'my_module.custom_exception_handler'}配置完成后，每当你在项目里抛出error_codes.CREATE_ERROR，用户便会看到下面的错误信息：{"code":'CREATE_ERROR',"detail":'创建失败'}5.1.2 ErrorCode API 说明在创建ErrorCode对象时，除了可以传入必须的message参数，还支持传入许多可选的个性化参数：message: 必选，错误详情信息，可包含字符串模板变量code_num: 可选，数字错误代码，默认为 -1extra_formatter：可选，额外的错误信息格式化函数status_code: 该错误推荐使用的 HTTP 错误代码，默认为 400这些参数各自有着不同的用途，比如，通过定义extra_formatter属性，你可以调整APIError拼装错误信息message的逻辑。以下面的代码为例：# formatter 函数接收两个参数：默认错误信息、当前异常对象def_format_message(message,exc):# 将错误码拼装到错误信息前returnf'code:{exc.code}-{message}'classErrorCodes:foo=ErrorCode('foo message',extra_formatter=_format_message)当你抛出foo错误码时，由于我们使用了自定义的message格式化函数，错误详情会变成这样：code: foo - foo message。ErrorCode的message除了能使用普通字符串以外，还支持字符串模板功能。举个例子，假如你定义的message是name={name}，那么，当你抛出异常时，可以用.format()方法传入模板变量，对错误信息进行二次渲染。raiseerror_codes.FOO# 1raiseerror_codes.FOO.format(name='foobar')# 2用户看到的错误信息是name={name}用户看到的错误信息会变为name=foobar5.2 blue_krill.web.drf_utilsdrf_utils模块内包含许多与 DRF 框架有关的工具。5.2.1 stringify_validation_errorstringify_validation_error()会将由 DRF 框架抛出的ValidationError校验错误异常对象，转换为可读性更好的错误提示文字。比如，下面的异常对象：ValidationError({'foo':{'bar':[ErrorDetail('err1'),ErrorDetail('err2')]}})可以被转换为：['foo.bar: err1', 'foo.bar: err2']这样的文字内容。6. blue_krill.async_utils6.1 blue_krill.aysnc_utils.poll_taskspoll_tasks是一个用来执行长时间轮询任务的异步工具模块。它的工作原理是每隔几秒钟，拉起一个celery任务进行轮询逻辑。当轮询应该结束时，带着结果回调。要创建一个新的轮询任务，你首先要编写一个TaskPoller类。fromblue_krill.async_utils.poll_taskimportTaskPoller,PollingResultclassMyTaskPoller(TaskPoller):# 通过定义下面的属性，修改当前 Poller 类的默认配置# max_retries_on_error = 10# overall_timeout_seconds = 3600 * 24 * 7# default_retry_delay_seconds = 10defquery(self)->PollingResult:result=request_api()ifresult:returnPollingResult.done(data={'result':...})else:returnPollingResult.doing(data={'current_value':...})TaskPoller的配置属性含义如下：max_retries_on_error：当轮询抛出异常的总次数，超过该值后，不再继续下次轮询overall_timeout_seconds：当轮询的总执行时间（从第一次轮询开始后计算）超过该值，结束本次轮询并返回超时结果default_retry_delay_seconds：两次轮询行为之间相隔的秒数每个TaskPoller类都必须重写query()方法，在其中实现每次轮询的真正逻辑。在query()方法内部，你可以从以下属性读取与本次轮询相关的数据：self.params：轮询任务启动时的参数，通常为字典Dictself.metadata：本次轮询任务的元数据，里面包含轮询开始时间、已完成的轮询次数等数据query()方法需要返回一个PollingResult结果，来控制接下来的轮询流程。不同的轮询结果，代表着不同含义：PollingResult.done()：表示整个轮询任务已结束，不会启动新异步任务PollingResult.doing()：表示应该继续轮询，会派生新的异步任务在实例化PollingResult时，你可以通过data属性传入额外数据。该数据对于不同状态的轮询结果来说，有着不同含义。当轮询返回done()结果时，data会通过回调传递到CallbackHandler的result参数里当轮询仍在继续，返回doing()结果时，可在TaskPoller类中，通过self.metadata.last_polling_data获取上次轮询的data内容创建完 TaskPoller 类后，下一步是编写 ResultHandler 结果回调类。6.1.1 定义 CallbackHandler 类一个标准的CallbackHandler如下所示：fromblue_krill.async_utils.poll_taskimport(CallbackHandler,CallbackResult,TaskPoller)classMyHandler(CallbackHandler):defhandle(self,result:CallbackResult,poller:TaskPoller):# 通过 result 和 poller 执行回调逻辑pass根据轮询的不同执行结果，CallbackResult.status会有几种不同的状态：CallbackStatus.NORMAL：轮询正常结束，Poller返回的轮询结果为DONE/ '.doing()'CallbackStatus.TIMEOUT：轮询超过了规定时间（overall_timeout_seconds）仍未结束，判定为超时CallbackStatus.EXCEPTION：轮询时发生了异常，且总异常次数超过最大值：max_retries_on_error为了方便操作，你可以直接调用result.is_exception属性来获知本次轮询是否正常结束。任何状态不为NORMAL的结果，.is_exception值都为True。6.1.2 启动轮询任务当你定义好Poller与CallbackHandler类后，可以用以下方式启动一次轮询任务：# params = {'some_field': 'value'}MyTaskPoller.start(params,MyHandler)通过执行TaskPoller类的start()方法，程序会派生出一个名为poll_task.check_status_until_finished的celery异步任务，之后触发TaskPoller的query()方法，不断开始轮询。6.2 blue_krill.aysnc_utils.django_utils这个模块提供了 Django + Celery 相关的一些辅助函数。6.2.1 apply_async_on_commit在开启 Django 事务的过程中，可能会立即触发一些 Celery 异步任务，在事务未提交或回滚时，异步任务执行结果是不可预期的。
这个函数可以封装 Celery 的异步任务调用：如果不在事务之中，会立刻触发异步任务；如果处于事务之中，则会在事务提交后触发；因为执行时机不确定，这个函数会强制忽略异步任务的返回值。6.2.2 delay_on_commit函数apply_async_on_commit的简化版本，相当于 Celery 中apply_async和delay的区别。7. blue_krill.monitoring.probeblue_krill.monitoring.probe模块提供了常见的健康探针功能。7.1 blue_krill.monitoring.probe.tcpblue_krill.monitoring.probe.tcp模块提供了通用的 TCP 健康探针, 可检测是否能建立 TCP 连接。# Usage:fromblue_krill.monitoring.probe.tcpimportTCPProbe,InternetAddressclassSomeTCPProbe(TCPProbe):name:str="some"address:InternetAddress=InternetAddress(host="localhost",port=8080)report=SomeTCPProbe().report()7.2 blue_krill.monitoring.probe.httpblue_krill.monitoring.probe.http模块提供了通用的 HTTP 健康探针, 可检测 HTTP 服务是否正常工作。# Usagefromblue_krill.monitoring.probe.httpimportHttpProbeclassSomeHttpProbe(HttpProbe):name:str="some"url:str="http://localhost/ping"report=SomeHttpProbe().report()classSomeHttpWithAuth(HttpProbe):name:str="some"url:str="http://localhost/ping"params:Dict={"token":"dummy"}headers:Dict={"Authorization":"Basic YWxhZGRpbjpvcGVuc2VzYW1l"}report=SomeHttpWithAuth().report()7.3 blue_krill.monitoring.probe.mysqlblue_krill.monitoring.probe.mysql模块提供了通用的 MySQL 健康探针, 可检测 MySQL 服务是否正常工作, 该模块依赖 pymysql。# Usage:fromblue_krill.monitoring.probe.mysqlimportMySQLProbe,MySQLConfigclassSomeMySQLProbe(MySQLProbe):name:str="some"config=MySQLConfig(host="localhost",port=3306,username="root",password="root",database="information_schema")report=SomeMySQLProbe().report()7.4 blue_krill.monitoring.probe.redisblue_krill.monitoring.probe.redis模块提供了通用的 Redis 健康探针和 Redis Sentinel 集群健康探针, 可检测 Redis 服务是否正常工作, 该模块依赖 redis。# Usage:fromblue_krill.monitoring.probe.redisimportRedisProbeclassSomeRedisProbe(RedisProbe):name:str="some"redis_url:str="redis://localhost:6379/0"report=SomeRedisProbe().report()# Redis Sentinelfromblue_krill.monitoring.probe.redisimportRedisSentinelProbeclassSomeRedisSentinelProbe(RedisSentinelProbe):name:str="some"redis_url:str="sentinel://:xxx@localhost:6379/0"master_name:str="mycluster"sentinel_kwargs:dict={'password':'xxx'}report=SomeRedisSentinelProbe().report()8 blue_krill.cubing_caseblue_krill.cubing_case增加各个方法互相转换的工具库.8.1 blue_krill.cubing_case.RegexCubingHelper基于多种正则将多种模式混合的字符串进行拆分，转换并组合成新的字符串的工具类。8.2 blue_krill.cubing_case.CommonCaseConvertor在blue_krill.cubing_case.RegexCubingHelper之上的一个封装实现，将指定的多种模式的字符串转化成常见的方法，包含：驼峰式：CubingCase小写开头的驼峰式：cubingCase小写下划线式：cubing_case大写下划线式：CUBING_CASE小写连字符式：cubing-case大写下划线式：CUBING-CASE小写点分式：cubing.case大写下划线式：CUBING.CASE小写空格分隔式：cubing case8.3 blue_krill.cubing_case.shortcutsblue_krill.cubing_case.shortcuts是blue_krill.cubing_case.CommonCaseConvertor的一个快捷方式，内置了其转换目标的所有源模式，可以实现所有模式的正反转换。9 blue_krill.redis_toolsblue_krill.redis_tools提供了 redis 常用工具9.1 blue_krill.redis_tools.sentinelblue_krill.redis_tools.sentinel提供了 redis sentinel 模式下，直接从 url 生成 redis 实例的方法。# Usage:fromblue_krill.redis_tools.sentinelimportSentinelBackendbackend=SentinelBackend('sentinel://xxx@localhost:26347/0','mycluster',{'password':'xxx'})r=backend.clientr.set('foo','bar')# 获得 b'bar'r.get('foo')10 blue_krill.encrypt.handlerblue_krill.encrypt.handler提够 Fernet 和 SM4 两种对称加密算法，并且为了适应存量数据，在解密时会根据header选择相应的算法进行解密。
具体的使用方式如下:fromblue_krill.encrypt.handlerimportEncryptHandler# 实例化# 第一种方式通过传入 encrypt_cipher_type(加密算法类型) 和 secret_key(密钥)# encrypt_cipher_type 现有的就是 "FernetCipher" 和 "SM4CTR"，分别对应 Fernet 和 SM4 对称加密算法encrypt_handler=EncryptHandler(encrypt_cipher_type='FernetCipher',secret_key=b'PIMCuSRiVqBg5eSzQqZZrOhGFSUtrlS-8_JlIpjHt0A=')# 第二种方式，不传入参数时，即 encrypt_cipher_type 和 secret_key 为 None# 会分别通过 django setting 中的 ENCRYPT_CIPHER_TYPE 和 BKKRILL_ENCRYPT_SECRET_KEY 字段设置。encrypt_handler=EncryptHandler()# 加解密使用text="random_text"# 加密，根据选择的算法不同，header 也会不同，算法为 Fernet 加密头为 bkcrypt$，算法为 SM4 时，加密头为 sm4ctr$# encrypted = "bkcrypt$gAAAAABkyIHPPbOeAOLa3LMc8901rslfBeTdm3rWZntSz5ut7eIDyb9eDgPmzVtL3y-iUBPSxRtZLC2ynlmeKeCNmRmTHpjtWg=="# encrypted = "sm4ctr$KI9M5PrhDCmj5ix90OKg/5qYLcR8F3owLlsG"encrypted=encrypt_handler.encrypt(text)# 解密# decrypted = "random_text"decrypted=encrypt_handler.decrypt(encrypted)11 blue_krill.models.fieldsblue_krill.models.fields基于EncryptHandler实现了EncryptField，具体使用：fromdjango.dbimportmodelsfromblue_krill.models.fieldsimportEncryptFieldclassUser(models.Model):"""User.password 在存取时会做加解密"""name=models.CharField(max_length=30)# EncryptField 用法与 EncryptHandler 类似# 实例化时，可传入 encrypt_cipher_type 选择加密算法，secret_key 配置密钥# 不传入时，会分别通过 django setting 中的 ENCRYPT_CIPHER_TYPE 和 BKKRILL_ENCRYPT_SECRET_KEY 字段设置。password=EncryptField()def__str__(self):returnself.name开发指南首先安装poetry，之后在项目目录下执行poetry env use python3.6初始化开发用虚拟环境。然后用poetry shell命令激活虚拟环境。执行poetry install安装所有依赖使用pytest -s .执行所有单元测试在开发时，如果想让某项目安装本地目录里的 blue-krill 模块，首先切换到对应项目虚拟环境，然后在 blue-krill 目录执行pip install -e .使用 tox 执行单元测试为了测试包在不同 Python 版本下的稳定性，我们使用了 tox 工具。在项目目录下执行tox即可执行所有的单元测试。发布包首先，执行poetry build命令在 dist 目录下生成当前版本的包(需要检查 dist 目录中的内容是否符合预期，避免上传其他版本覆盖)。然后执行twine upload dist/* --repository-url {pypi_address} --username {your_name} --password {your_token}将其上传到 pypi 服务器上。以“可编辑模式”安装进入项目根目录，执行pip install -e .以“可编辑模式”安装包。该操作要求 pip 为版本 21.3 及以上（参考）。