From 9af4cb918bba832ac137052a73cd009d31481592 Mon Sep 17 00:00:00 2001 From: "solar.hu" Date: Fri, 19 Jun 2020 16:50:12 +0800 Subject: [PATCH 01/72] init packageship dir and add design doc --- .../doc/design/packageManagerDesigen.md | 223 ++++++++++++++++++ packageship/doc/design/pkgimg/Package.JPG | Bin 0 -> 19687 bytes packageship/doc/design/pkgimg/Repodatas.JPG | Bin 0 -> 11020 bytes packageship/doc/design/pkgimg/beDepend_1.JPG | Bin 0 -> 58558 bytes packageship/doc/design/pkgimg/beDepend_2.JPG | Bin 0 -> 70965 bytes .../doc/design/pkgimg/buildDepend_1.JPG | Bin 0 -> 49393 bytes .../doc/design/pkgimg/depend_flowchart.png | Bin 0 -> 181735 bytes .../doc/design/pkgimg/installDepend.JPG | Bin 0 -> 39336 bytes .../doc/design/pkgimg/packagemanagement.JPG | Bin 0 -> 95650 bytes 9 files changed, 223 insertions(+) create mode 100644 packageship/doc/design/packageManagerDesigen.md create mode 100644 packageship/doc/design/pkgimg/Package.JPG create mode 100644 packageship/doc/design/pkgimg/Repodatas.JPG create mode 100644 packageship/doc/design/pkgimg/beDepend_1.JPG create mode 100644 packageship/doc/design/pkgimg/beDepend_2.JPG create mode 100644 packageship/doc/design/pkgimg/buildDepend_1.JPG create mode 100644 packageship/doc/design/pkgimg/depend_flowchart.png create mode 100644 packageship/doc/design/pkgimg/installDepend.JPG create mode 100644 packageship/doc/design/pkgimg/packagemanagement.JPG diff --git a/packageship/doc/design/packageManagerDesigen.md b/packageship/doc/design/packageManagerDesigen.md new file mode 100644 index 00000000..be6278c6 --- /dev/null +++ b/packageship/doc/design/packageManagerDesigen.md @@ -0,0 +1,223 @@ +#特性描述 +管理OS软件包依赖关系,提供依赖和被依赖关系的完整图谱查询功能,方便开发者识别软件包范围,减少依赖梳理复杂度。 +##原始需求-软件包依赖管理 +- 输入软件包A,支持查询A的所有编译依赖(新增软件包) +- 输入软件包A,支持查询A的所有安装依赖(新增软件包) +- 输入软件包A,支持查询所有安装依赖A的软件(升级,删除软件包场景) +- 输入软件包A,支持查询所有编译依赖A的软件(升级,删除软件包场景) + +#依赖组件 +- createrepo + +#License +Mulan V2 + +#流程分析 +##软件包依赖管理 +![avatar](./pkgimg/packagemanagement.JPG) +###功能清单 +- SR-PKG-MANAGE01-AR01:支持repo数据库导入 +- SR-PKG-MANAGE01-AR02:支持对多个数据库分级查询(内部接口) +- SR-PKG-MANAGE01-AR03:支持软件包安装/编译依赖查询 +- SR-PKG-MANAGE01-AR04:支持软件包自编译/自安装依赖查询 +- SR-PKG-MANAGE01-AR05:支持被依赖查询 +- SR-PKG-MANAGE01-AR06:支持编译被依赖查询 +- SR-PKG-MANAGE01-AR07:支持前台查询和显示软件依赖关系 +在线评审意见平台,支持查询评审意见及溯源 + +##外部接口清单 + +| 序号 | 接口名称 | 类型 | 说明 | 入参 | 出参 | 特性号 | +| - | - | - | - | - | - | - | +| 1 | /packages | GET | 支持查看所有软件包信息 | dbName | *packages* | AR01 & AR02 | +| 2 | /packages | PUT | 支持更新指定软件包的信息 | *packages* | null | AR01 | +| 3 | /packages/findByPackName | GET | 支持查询指定软件包的信息 | packageName,dbName,version(option) | *packages* | AR01 & AR02 | +| 4 | /packages/findInstallDepend | POST | 支持查询指定软件包安装依赖(在一个或多个数据库中分级查询) | packageName,version(opetion),dbPreority | *response* | AR02 & AR03 | +| 5 | /packages/findBuildDepend | POST | 支持查询指定软件包的编译依赖(在一个或多个数据库中分级查询) | packageName、version、repoPreority | *response* | AR02 & AR03 | +| 6 | /packages/findSelfDepend | POST | 支持查询指定软件包的自安装/自编译依赖(在一个或多个数据库中分级查询) | packageName、version、repoPreority、withSubPack、withSelfBuild | packageName、installDepend、buildDepend、parentNode | AR02 & AR04 | +| 7 | /packages/findBeDepend | POST | 支持在数据库中查询指定软件包的所有被依赖 | packageName、version、repoPreority、withSubPack | packageName、installBeDepend、buildBeDepend、parentNode | AR05 | +| 8 | /repodatas | GET | 支持获取所有引入的版本库 | null | *Repodatas | AR01 | +| 9 | /repodatas | POST | 支持repo数据库的导入 | dbName、dbPath、priority、dbStatus、repofiles | null | AR01 | +| 10 | /repodatas | PUT | 支持版本库的更新 | dbName、dbPath、priority、dbStatus | null | AR01 | + +###python函数接口清单 + +| 序号 | 接口名称 | 说明 | 入参 | 出参 | +| - | - | - | - | - | +| 1 | get_packages | 支持查看所有软件包信息 | dbName | *packages* | +| 2 | update_package | 支持更新指定软件包信息 | *package* | null | +| 3 | query_package | 支持查询指定软件包的信息 | source_name,dbname,version(option) | *package* | +| 4 | query_install_depend | 支持查询指定软件包安装依赖(在一个或多个数据库中分级查询) | binary_name,version(option), db_preority | *response* | +| 5 | query_build_depend | 支持查询指定软件包的编译依赖(在一个或多个数据库中分级查询) | source_name,version(option),db_preority,selfbuild=1/0 | *response* | +| 6 | query_subpack | 支持查询指定源码包的子包 | srouce_name,version(option),db_preority | *subpack_list* | +| 7 | query_srcpack | 支持查询指定二进制包的源码包 | binary_name,version(option),dbname | srouce_name | +| 6 | query_self_depend | 支持查询指定软件包的自安装/自编译依赖(在一个或多个数据库中分级查询) | package_name,version(option),db_preority,withsubpack(default:0),withselfbuild(default:0) | *response* | +| 7 | query_self_be_depend | 支持在数据库中查询指定源码包的所有被依赖(在一个或多个数据库中分级查询) | source_name,version(option),db_preority,withsubpack(default:0) | *response* | +| 8 | get_db | 支持获取所有引入的版本库 | null | *dbinfo* | +| 9 | import_db | 支持repo数据库的导入 | *dbinfo* | null | +| 10 | update_db | 支持版本库的更新 | *dbinfo* | null | + +###外部config文件输入格式清单 +1.初始化配置文件(init_db.config) +``` +#dbname - 数据库名称,unique,不可重复 +# src_db_file - 包含源码包信息的sqlite 文件 +# bin_db_file - 包含二进制包信息的sqlite 文件 +# status - 数据库状态,enable表示可用,disable表示不可用 +# priority - 1~100 default priority for user to query the information in databases + +- dbname: openEuler-20.03-LTS + src_db_file: /etc/pkgmng/dbname/primary_src.sqlite + bin_db_file: /etc/pkgmng/dbname/primary_binary.sqlite + status: enable + priority: 1 + +- dbname: openEuler-20.04-LTS + src_db_file: testdb/src + bin_db_file: testdb/bin + status: enable + priority: 2 + +- dbname: openEuler-20.05-LTS + src_db_file: testdb/src + bin_db_file: testdb/bin + status: enable + priority: 3 +``` +2.更新数据库信息(update_db.config) +``` +- dbname: openEuler-20.03-LTS + changeDBname: openEuler-LTS + addDBFile: /etc/pkgmng/dbname/primary1.sqlite + removeDBFile: /etc/pkgmng/dbname/primary2.sqlite + status: disable + priority: 4 +``` + +3.更新包的信息(package.config) +``` +#level: 维护的优先级,1-4 +- dbname: openEuler-20.03-LTS + packageName: openssh + version: 2.99 + maintainer: solar-hu + level: 3 +``` +###object +``` + + openssh + 1.3.2 + 2-66 + GLv2 + solar-hu + http://linuxcontainers.org + lxc-4.0.1.tar.gz + openEuler-20.03-LTS + + zip-devel + libmediaart-devel + + + openssh-devel + + maven-public + tomcat + + openssh-static + openssh-help + + +``` + +``` + + openEuler + 4 + enable + +``` + +#数据表设计 +- src-pack + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | 默认值 | +| - | - | - | - | - | - | - | +| 1 | id | 源码包条目序号 | Int | Primary | NO | - | +| 2 | name | 源码包包名 | String | | NO | - | +| 3 | version | 版本号 | String | | NO | - | +| 4 | license | 证书 | String |  | NO | -  | +| 5 | sourceURL | 源码包获取地址 | String |  | YES | -  | +| 6 | downloadURL | 下载地址获取 | String |  | YES | -  | +| 7 | Maintaniner | 维护责任人 | String |  | YES | -  | +| 8 | MaintainLevel | 维护优先级 | String |  | YES | -  | + +- bin-pack + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | 默认值 | +| - | - | - | - | - | - | - | +| 1 | id | 二进制包条目序号 | Int | Primary | NO | -  | +| 2 | name | 二进制包包名 | String |  | NO | -  | +| 3 | version | 版本号 | String |  | NO | -  | +| 4 | srcIDkey | 源码包包名ID | Int | foreignkey | NO | -  | + +- pack-requires + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | 默认值 | +| - | - | - | - | - | - | - | +| 1 | id | 依赖组件条目序号 | Int | Primary | NO | -  | +| 2 | name | 依赖组件名 | String |  | NO | -  | +| 3 | depProIDkey | 依赖组件对应的ID | Int | foreignkey | NO | -  | +| 4 | srcIDkey | 若为源码包该值不为空,列出来的是编译依赖 | Int | foreignkey | YES | -  | +| 5 | binIDkey | 若为安装包该值不为空,列出来的是安装依赖 | Int | foreignkey | YES | -  | + +- pack-provides + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | 默认值 | +| - | - | - | - | - | - | - | +| 1 | id | 组件条目序号 | Int | Primary | NO | -  | +| 2 | name | 组件名 | Int | Primary | NO | -  | +| 3 | binIDkey | 提供组件的二进制包ID | Int | foreignkey | NO | -  | + + +- repoCheckSame + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | 默认值 | +| - | - | - | - | - | - | - | +| 1 | id | repoFile条目序号 | Int | Primary | NO | -  | +| 2 | name | repoFile名称 | String | | NO | -  | +| 3 | md5sum | md5sum指 | String | | NO | -  | + + + +#功能设计 +##主体流程分析 +![avatar](./pkgimg/depend_flowchart.png) + +##依赖关系梳理 +findInstallDepend: +![avatar](./pkgimg/installDepend.JPG) +findBuildDepend: +![avatar](./pkgimg/buildDepend_1.JPG) +findBeDepend(withSubPack = 0): +删除源码包A造成的影响: +1.影响他的子包(A1,A2) +2.安装依赖A1,A2的二进制包 +3.编译依赖A1,A2的源码包 +![avatar](./pkgimg/beDepend_1.JPG) +findBeDepend(withSubPack = 1): +删除源码包A造成的影响: +1.影响他的子包(A1,A2) +2.安装依赖A1,A2的二进制包(B1) +3.编译依赖A1,A2的源码包 +4.删除B1的源码包B,影响B的其他子包B2,B3 +![avatar](./pkgimg/beDepend_2.JPG) + +#遗留问题 +- repo数据库分析,如何做数据组织 汪奕如 +- 嵌套依赖查询流程整理 汪奕如 +- svn/git监控原型验证 陈燕潘 +- gitee机机接口对齐 陈燕潘 +- 版本升级如何更新到补丁获取系统中 陈燕潘 +- web前台拓扑图UCD设计 NA +- 数据表设计 汪奕如 diff --git a/packageship/doc/design/pkgimg/Package.JPG b/packageship/doc/design/pkgimg/Package.JPG new file mode 100644 index 0000000000000000000000000000000000000000..775833d113cc0d3c975fe397abb8fa73034f24da GIT binary patch literal 19687 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 AzOIM=$J4GY?=mr4i!QJqMP}Zk!BmqkseSdFRSo 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 literal 0 HcmV?d00001 diff --git a/packageship/doc/design/pkgimg/beDepend_1.JPG b/packageship/doc/design/pkgimg/beDepend_1.JPG new file mode 100644 index 0000000000000000000000000000000000000000..8dfd56a92936f14f2afe587926ca06f5c027f2e5 GIT binary patch literal 58558 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 z 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

^ELjg6DfP$ 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 literal 0 HcmV?d00001 diff --git a/packageship/doc/design/pkgimg/beDepend_2.JPG b/packageship/doc/design/pkgimg/beDepend_2.JPG new file mode 100644 index 0000000000000000000000000000000000000000..4db41a623a7e32f01d1353e4f966a89755b151ac GIT binary patch literal 70965 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 WiATnc??aA&KMhr#+l9(D&;B3RF}vLW literal 0 HcmV?d00001 diff --git a/packageship/doc/design/pkgimg/buildDepend_1.JPG b/packageship/doc/design/pkgimg/buildDepend_1.JPG new file mode 100644 index 0000000000000000000000000000000000000000..cd01369fbe5e3a4075c603a62adf5252b24a2401 GIT binary patch literal 49393 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¥kDrZmxh!vlmI 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 literal 0 HcmV?d00001 diff --git a/packageship/doc/design/pkgimg/depend_flowchart.png b/packageship/doc/design/pkgimg/depend_flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..7c6447afa562b3d981df8a355b043d4ee2dadef9 GIT binary patch literal 181735 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 A^8f$< literal 0 HcmV?d00001 diff --git a/packageship/doc/design/pkgimg/installDepend.JPG b/packageship/doc/design/pkgimg/installDepend.JPG new file mode 100644 index 0000000000000000000000000000000000000000..0f35928860281ca6ca249200e6313dc63dab621f GIT binary patch literal 39336 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 literal 0 HcmV?d00001 diff --git a/packageship/doc/design/pkgimg/packagemanagement.JPG b/packageship/doc/design/pkgimg/packagemanagement.JPG new file mode 100644 index 0000000000000000000000000000000000000000..df7151a74e051051eaeb3fb16a3abb5f0e0d2e37 GIT binary patch literal 95650 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 Rc6q^p7aaJ1all|w_+J>mYq|gc literal 0 HcmV?d00001 -- Gitee From 1c8c2d6b012e8d7e5b22c5d7ccbfdc39bbfda8c0 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Sat, 20 Jun 2020 11:03:32 +0800 Subject: [PATCH 02/72] Set up the basic framework and restful API for packageship, including service startup file, flask buleprint registration, database establishment and common operation method, configuration file reading, log file configuration and exception capture --- packageship/.gitignore | 2 + packageship/README.md | 84 +++++++ packageship/packageship/__init__.py | 0 .../packageship/application/__init__.py | 48 ++++ .../packageship/application/app_global.py | 25 ++ .../packageship/application/apps/__init__.py | 7 + .../application/apps/package/__init__.py | 16 ++ .../apps/package/function/__init__.py | 0 .../application/apps/package/url.py | 35 +++ .../application/apps/package/view.py | 234 ++++++++++++++++++ .../application/models/__init__.py | 0 .../application/setting/__init__.py | 14 ++ .../packageship/application/setting/dev.py | 9 + .../packageship/application/setting/pro.py | 9 + .../packageship/libs/configutils/__init__.py | 0 .../libs/configutils/readconfig.py | 56 +++++ .../packageship/libs/dbutils/__init__.py | 3 + .../libs/dbutils/sqlalchemy_helper.py | 189 ++++++++++++++ .../packageship/libs/exception/__init__.py | 8 + packageship/packageship/libs/exception/ext.py | 48 ++++ packageship/packageship/libs/log/__init__.py | 4 + packageship/packageship/libs/log/loghelper.py | 93 +++++++ packageship/packageship/manage.ini | 13 + packageship/packageship/manage.py | 44 ++++ packageship/packageship/package.ini | 60 +++++ packageship/packageship/selfpkg.ini | 13 + packageship/packageship/selfpkg.py | 43 ++++ packageship/packageship/system_config.py | 35 +++ packageship/setup.py | 73 ++++++ 29 files changed, 1165 insertions(+) create mode 100644 packageship/.gitignore create mode 100644 packageship/README.md create mode 100644 packageship/packageship/__init__.py create mode 100644 packageship/packageship/application/__init__.py create mode 100644 packageship/packageship/application/app_global.py create mode 100644 packageship/packageship/application/apps/__init__.py create mode 100644 packageship/packageship/application/apps/package/__init__.py create mode 100644 packageship/packageship/application/apps/package/function/__init__.py create mode 100644 packageship/packageship/application/apps/package/url.py create mode 100644 packageship/packageship/application/apps/package/view.py create mode 100644 packageship/packageship/application/models/__init__.py create mode 100644 packageship/packageship/application/setting/__init__.py create mode 100644 packageship/packageship/application/setting/dev.py create mode 100644 packageship/packageship/application/setting/pro.py create mode 100644 packageship/packageship/libs/configutils/__init__.py create mode 100644 packageship/packageship/libs/configutils/readconfig.py create mode 100644 packageship/packageship/libs/dbutils/__init__.py create mode 100644 packageship/packageship/libs/dbutils/sqlalchemy_helper.py create mode 100644 packageship/packageship/libs/exception/__init__.py create mode 100644 packageship/packageship/libs/exception/ext.py create mode 100644 packageship/packageship/libs/log/__init__.py create mode 100644 packageship/packageship/libs/log/loghelper.py create mode 100644 packageship/packageship/manage.ini create mode 100644 packageship/packageship/manage.py create mode 100644 packageship/packageship/package.ini create mode 100644 packageship/packageship/selfpkg.ini create mode 100644 packageship/packageship/selfpkg.py create mode 100644 packageship/packageship/system_config.py create mode 100644 packageship/setup.py diff --git a/packageship/.gitignore b/packageship/.gitignore new file mode 100644 index 00000000..04ed6add --- /dev/null +++ b/packageship/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*/.DS_Store \ No newline at end of file diff --git a/packageship/README.md b/packageship/README.md new file mode 100644 index 00000000..1639cd21 --- /dev/null +++ b/packageship/README.md @@ -0,0 +1,84 @@ +# pkgmnt + +#### 介绍 +pkgmnt希望提供软件包依赖,生命周期,补丁查询等功能。 +1.软件包依赖:方便社区人员在新引入、软件包更新和删除的时候能方便的了解软件的影响范围。 +2.生命周期管理:跟踪upstream软件包发布状态,方便维护人员了解当前软件状态,及时升级到合理的版本。 +3.补丁查询:方便社区人员了解openEuler软件包的补丁情况,方便的提取补丁内容(待规划) + + +#### 软件架构 +系统采用flask-restful开发,使用SQLAlchemy ORM查询框架,同时支持mysql和sqlite数据库,通过配置文件的 +形式进行更改 + + +#### 安装教程 + +1. 安装系统的依赖包 + + pip install -r requirements.txt + +2. 执行打包命令,打包命令行工具,其中(pkgship)为命令行的名称,可以随意更改 + + 2.1 打包生成 .spec打包文件 + + pyinstaller -F -n pkgship cli.py + + 2.2 修改 .spec打包文件,将hiddenimports中加入如下配置 + + hiddenimports=['pkg_resources.py2_warn'] + + 2.3 生成二进制命令文件 + + pyinstaller pkgship.spec + + 2.4 二进制命令文件拷贝至可运行目录 + + cp dist/pkgship /usr/local/bin + +3. 系统的部署 + + 3.1 安装uwsgi服务器 + + pip install uwsgi + + 3.2 修改服务的配置文件 + + cd /etc/pkgship/ + + vi package.ini + + 备注: 配置文件中可以支撑sqlite数据库和mysql数据库,可根据相应配置进行修改 + + 如果需要调整 查询和修改相关端口,请同步更改 mange.ini 和selfpkg.ini 中的配置 + + 切记(manage.py为拥有写入权限,selfpkg为拥有查询权限) + + 3.3 启动系统服务 + + uwsgi -d --ini manage.ini + + uwsgi -d --ini selfpkg.ini + + +#### 使用说明 + +1. 命令行使用 + + pkgship --help + +2. restful接口使用 + + 参考接口设计文档中的接口定义,进行相关接口调用 + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 +4. 新建 Pull Request + + +#### 会议记录 +1. 2020.5.18:https://etherpad.openeuler.org/p/aHIX4005bTY1OHtOd_Zc + diff --git a/packageship/packageship/__init__.py b/packageship/packageship/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/packageship/application/__init__.py b/packageship/packageship/application/__init__.py new file mode 100644 index 00000000..89bcd028 --- /dev/null +++ b/packageship/packageship/application/__init__.py @@ -0,0 +1,48 @@ +''' + Initial operation and configuration of the flask project +''' +from flask import Flask +from flask_session import Session +from packageship.application.settings.dev import DevelopementConfig +from packageship.application.settings.pro import ProductionConfig +from packageship.libs.log import setup_log + + +# development and production environment configuration + +CONFIG = { + 'dev': DevelopementConfig, + 'prop': ProductionConfig +} + +OPERATION = None + + +def init_app(config_name, operation): + ''' + Project initialization function + ''' + app = Flask(__name__) + + config = CONFIG[config_name] + + # log configuration + setup_log(config) + + # Load configuration items + + app.config.from_object(config) + + # Open session function + Session(app) + + global OPERATION + OPERATION = operation + + # Register Blueprint + from packageship.application.apps import blue_point + for blue, api in blue_point: + api.init_app(app) + app.register_blueprint(blue) + + return app diff --git a/packageship/packageship/application/app_global.py b/packageship/packageship/application/app_global.py new file mode 100644 index 00000000..2684a6fc --- /dev/null +++ b/packageship/packageship/application/app_global.py @@ -0,0 +1,25 @@ +''' + Interception before request +''' +from flask import request +from packageship.application import OPERATION +from packageship.application.apps.package.url import urls + + +__all__ = ['identity_verification'] + + +def identity_verification(): + ''' + Requested authentication + ''' + if request.url_rule: + url_rule = request.url_rule.rule + for view, url, authentication in urls: + if url == url_rule and OPERATION in authentication.keys(): + if request.method not in authentication.get(OPERATION): + return False + break + return True + + return False diff --git a/packageship/packageship/application/apps/__init__.py b/packageship/packageship/application/apps/__init__.py new file mode 100644 index 00000000..7734e498 --- /dev/null +++ b/packageship/packageship/application/apps/__init__.py @@ -0,0 +1,7 @@ +from packageship.application.apps.package import package, api as package_api + +blue_point = [ + (package, package_api) +] + +__all__ = ['blue_point'] diff --git a/packageship/packageship/application/apps/package/__init__.py b/packageship/packageship/application/apps/package/__init__.py new file mode 100644 index 00000000..d054951e --- /dev/null +++ b/packageship/packageship/application/apps/package/__init__.py @@ -0,0 +1,16 @@ +from flask.blueprints import Blueprint +from flask_restful import Api +from packageship.application.apps.package.url import urls +from packageship.application import OPERATION + +package = Blueprint('package', __name__) + +# init restapi +api = Api() + +for view, url, operation in urls: + if OPERATION and OPERATION in operation.keys(): + api.add_resource(view, url) + + +__all__ = ['package', 'api'] diff --git a/packageship/packageship/application/apps/package/function/__init__.py b/packageship/packageship/application/apps/package/function/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/packageship/application/apps/package/url.py b/packageship/packageship/application/apps/package/url.py new file mode 100644 index 00000000..1e12fa78 --- /dev/null +++ b/packageship/packageship/application/apps/package/url.py @@ -0,0 +1,35 @@ +""" + url set +""" +from . import view + +urls = [ + # Get all packages' info + (view.Packages, '/packages', {'query': ('GET')}), + + + # Query and update a package info + (view.SinglePack, '/packages/findByPackName', + {'query': ('GET'), 'write': ('PUT')}), + + # Query a package's install depend(support querying in one or more databases) + (view.InstallDepend, '/packages/findInstallDepend', {'query': ('POST')}), + + # Query a package's build depend(support querying in one or more databases) + + (view.BuildDepend, '/packages/findBuildDepend', {'query': ('POST')}), + + # Query a package's all dependencies including install and build depend + # (support quering a binary or source package in one or more databases) + (view.SelfDepend, '/packages/findSelfDepend', {'query': ('POST')}), + + # Query a package's all be dependencies including be installed and built depend + (view.BeDepend, '/packages/findBeDepend', {'query': ('POST')}), + + # Get all imported databases, import new databases and update existed databases + + (view.Repodatas, '/repodatas', {'query': ('GET'), 'write': ('DELETE')}), + + # Reload database + (view.InitSystem, '/initsystem', {'write': ('POST')}) +] diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py new file mode 100644 index 00000000..16a76b27 --- /dev/null +++ b/packageship/packageship/application/apps/package/view.py @@ -0,0 +1,234 @@ +from flask import request +from flask_restful import Resource +from flask import jsonify +from flask import current_app +from sqlalchemy.exc import SQLAlchemyError + +from packageship.libs.log import Log + +LOGGER = Log(__name__) + +class Packages(Resource): + ''' + Description: interface for package info management + Restful API: get + changeLog: + ''' + + def get(self, *args, **kwargs): + ''' + Description: Get all package info from a database + input: + dbName + return: + json file contain package's info + Exception: + Changelog: + ''' + pass + + +class SinglePack(Resource): + ''' + description: single package management + Restful API: get、put + ChangeLog: + ''' + + def get(self, *args, **kwargs): + ''' + description: Searching a package info + input: + sourceName + dbName + return: + json file contain package's detailed info + exception: + changeLog: + ''' + pass + + def put(self, *args, **kwargs): + ''' + Description: update a package info + input: + packageName + dbName + maintainer + maintainLevel + return: + exception: + changeLog: + ''' + pass + + +class InstallDepend(Resource): + ''' + Description: install depend of binary package + Restful API: post + changeLog: + ''' + + def post(self, *args, **kwargs): + ''' + Description: Query a package's install depend(support + querying in one or more databases) + input: + binaryName + dbPreority:the array for database preority + return: + resultList[ + result[ + binaryName: binary package name + srcName: the source package name for + that binary packge + dbName: + type: install install or build, which + depend on the function + parentNode: the binary package name which is + the install depend for binaryName + ] + ] + exception: + changeLog: + ''' + pass + + +class BuildDepend(Resource): + ''' + Description: build depend of binary package + Restful API: post + changeLog: + ''' + def post(self, *args, **kwargs): + ''' + Description: Query a package's build depend and + build depend package's install depend + (support querying in one or more databases) + input: + sourceName : + dbPreority:the array for database preority + return: + resultList[ + restult[ + binaryName: + srcName: + dbName: + type: install or build, which depend + on the function + parentNode: the binary package name which is + the build/install depend for binaryName + ] + ] + exception: + changeLog: + ''' + pass + + +class SelfDepend(Resource): + ''' + Description: querying install and build depend for a package + and others which has the same src name + Restful API: post + changeLog: + ''' + + def post(self, *args, **kwargs): + ''' + description: Query a package's all dependencies including install and build depend + (support quering a binary or source package in one or more databases) + input: + packageName: + packageType: source/binary + selfBuild :0/1 + withSubpack: 0/1 + dbPreority:the array for database preority + return: + resultList[ + restult[ + binaryName: + srcName: + dbName: + type: install or build, which depend on the function + parentNode: the binary package name which is the + build/install depend for binaryName + ] + ] + + exception: + changeLog: + ''' + pass + + +class BeDepend(Resource): + ''' + Description: querying be installed and built depend for a package + and others which has the same src name + Restful API: post + changeLog: + ''' + + def post(self, *args, **kwargs): + ''' + description: Query a package's all dependencies including + be installed and built depend + input: + packageName: + withSubpack: 0/1 + dbname: + return: + resultList[ + restult[ + binaryName: + srcName: + dbName: + type: beinstall or bebuild, which depend on the function + childNode: the binary package name which is the be built/installed depend for binaryName + ] + ] + exception: + changeLog: + ''' + pass + + +class Repodatas(Resource): + """API for operating databases""" + + def get(self, *args, **kwargs): + ''' + description: get all database + input: + return: + databasesName + status + priority + exception: + changeLog: + ''' + pass + + def delete(self, *args, **kwargs): + ''' + description: get all database + input: database name + return: + ''' + +class InitSystem(Resource): + '''InitSystem''' + + def post(self, *args, **kwargs): + """ + description: InitSystem + input: + return: + exception: + changeLog: + """ + pass + diff --git a/packageship/packageship/application/models/__init__.py b/packageship/packageship/application/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/packageship/application/setting/__init__.py b/packageship/packageship/application/setting/__init__.py new file mode 100644 index 00000000..1d03095c --- /dev/null +++ b/packageship/packageship/application/setting/__init__.py @@ -0,0 +1,14 @@ + + +class Config(object): + ''' + Basic configuration items of the project + ''' + + # Debug mode + + DEBUG = True + + LOG_LEVEL = "DEBUG" + + SECRET_KEY = 'serwj1232aeaeq@adssr3rssa3123dvfdge53ad' diff --git a/packageship/packageship/application/setting/dev.py b/packageship/packageship/application/setting/dev.py new file mode 100644 index 00000000..73c6087a --- /dev/null +++ b/packageship/packageship/application/setting/dev.py @@ -0,0 +1,9 @@ +from . import Config + + +class DevelopementConfig(Config): + ''' + Configuration in development mode + ''' + + DEBUG = True diff --git a/packageship/packageship/application/setting/pro.py b/packageship/packageship/application/setting/pro.py new file mode 100644 index 00000000..121ec5fb --- /dev/null +++ b/packageship/packageship/application/setting/pro.py @@ -0,0 +1,9 @@ +from . import Config + + +class ProductionConfig(Config): + ''' + Configuration in development mode + ''' + + DEBUG = False diff --git a/packageship/packageship/libs/configutils/__init__.py b/packageship/packageship/libs/configutils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/packageship/libs/configutils/readconfig.py b/packageship/packageship/libs/configutils/readconfig.py new file mode 100644 index 00000000..c729ac61 --- /dev/null +++ b/packageship/packageship/libs/configutils/readconfig.py @@ -0,0 +1,56 @@ +''' + Read the base class of the configuration file in the system + which mainly includes obtaining specific node values + and obtaining arbitrary node values +''' +import os +import configparser +from configparser import NoSectionError +from configparser import NoOptionError +from packageship.system_config import SYS_CONFIG_PATH + + +class ReadConfig: + ''' + Read the configuration file base class in the system + ''' + + def __init__(self): + self.conf = configparser.ConfigParser() + self.conf.read(SYS_CONFIG_PATH) + + def get_system(self, param): + ''' + Get any data value under the system configuration node + ''' + if param: + try: + return self.conf.get("SYSTEM", param) + except NoSectionError: + return None + except NoOptionError: + return None + + def get_database(self, param): + ''' + Get any data value under the database configuration node + ''' + if param: + try: + return self.conf.get("DATABASE", param) + except NoSectionError: + return None + except NoOptionError: + return None + + def get_config(self, node, param): + ''' + Get configuration data under any node + ''' + if all([node, param]): + try: + return self.conf.get(node, param) + except NoSectionError: + return None + except NoOptionError: + return None diff --git a/packageship/packageship/libs/dbutils/__init__.py b/packageship/packageship/libs/dbutils/__init__.py new file mode 100644 index 00000000..c70f6e8f --- /dev/null +++ b/packageship/packageship/libs/dbutils/__init__.py @@ -0,0 +1,3 @@ +from .sqlalchemy_helper import DBHelper + +__all__ = ['DBHelper'] \ No newline at end of file diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py new file mode 100644 index 00000000..966aa6bd --- /dev/null +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -0,0 +1,189 @@ +''' +Simple encapsulation of sqlalchemy orm framework operation database + +''' +import os +from packageship.system_config import DATABASE_FOLDER_PATH +from sqlalchemy import create_engine +from sqlalchemy import MetaData +from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import DisconnectionError +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.engine.url import URL +from packageship.libs.exception.ext import Error +from packageship.libs.exception.ext import DbnameNoneException +from packageship.libs.exception.ext import ContentNoneException +from packageship.libs.configutils.readconfig import ReadConfig + + +class DBHelper(): + + # The base class inherited by the data model + BASE = declarative_base() + + def __init__(self, user_name=None, passwrod=None, ip_address=None, \ + port=None, db_name=None, db_type=None, *args, **kwargs): + self.user_name = user_name + self._readconfig = ReadConfig() + if self.user_name is None: + self.user_name = self._readconfig.get_database('user_name') + + self.passwrod = passwrod + if self.passwrod is None: + self.passwrod = self._readconfig.get_database('password') + + self.ip_address = ip_address + + if self.ip_address is None: + self.ip_address = self._readconfig.get_database('host') + + self.port = port + + if self.port is None: + self.port = self._readconfig.get_database('port') + + self.db_name = db_name + + if self.db_name is None: + self.db_name = self._readconfig.get_database('database') + + self.db_type = db_type + + if self.db_type is None: + # read the contents of the configuration file + _db_type = self._readconfig.get_database('dbtype') + if _db_type is None or _db_type == 'mysql': + self.db_type = 'mysql+pymysql' + else: + self.db_type = 'sqlite:///' + if 'import_database' not in kwargs.keys(): + self._db_file_path() + self.db_name = os.path.join( + self.database_file_path, self.db_name + '.db') + if self.db_type.startswith('sqlite'): + if not self.db_name: + raise DbnameNoneException( + 'The connected database name is empty') + self.engine = create_engine( + self.db_type + self.db_name, encoding='utf-8', convert_unicode=True) + else: + if all([self.user_name, self.passwrod, self.ip_address, self.port, self.db_name]): + # create connection object + self.engine = create_engine(URL(**{'database': self.db_name, + 'username': self.user_name, + 'password': self.passwrod, + 'host': self.ip_address, + 'port': self.port, + 'drivername': self.db_type}), \ + encoding='utf-8', \ + convert_unicode=True) + else: + raise DisconnectionError( + 'A disconnect is detected on a raw DB-API connection') + self.session = None + + def _db_file_path(self): + ''' + load the path stored in the sqlite database + ''' + self.database_file_path = self._readconfig.get_system( + 'data_base_path') + if not self.database_file_path: + self.database_file_path = DATABASE_FOLDER_PATH + if not os.path.exists(self.database_file_path): + os.makedirs(self.database_file_path) + + def __enter__(self): + ''' + functional description:Create a context manager for the database connection + ''' + + Session = sessionmaker() + if getattr(self, 'engine') is None: + raise DisconnectionError('Abnormal database connection') + Session.configure(bind=self.engine) + + self.session = Session() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + ''' + functional description:Release the database connection pool and close the connection + ''' + + self.session.close() + + @classmethod + def create_all(cls, db_name=None): + ''' + functional description:Create all database tables + parameter: + return value: + exception description: + modify record: + ''' + + cls.BASE.metadata.create_all(bind=cls(db_name=db_name).engine) + + def create_table(self, tables): + ''' + Create a single table + ''' + meta = MetaData(self.engine) + for table_name in DBHelper.BASE.metadata.tables.keys(): + from sqlalchemy import Table + if table_name in tables: + table = DBHelper.BASE.metadata.tables[table_name] + table.metadata = meta + table.create() + + def add(self, entity): + ''' + functional description:Insert a single data entity + parameter: + return value: + If the addition is successful, return the corresponding entity, otherwise return None + ''' + + if entity is None: + raise ContentNoneException( + 'The added entity content cannot be empty') + + try: + self.session.add(entity) + + except SQLAlchemyError as e: + raise Error(e) + else: + self.session.commit() + return entity + + def batch_add(self, dicts, model): + ''' + functional description:tables for adding databases in bulk + parameter: + :param dicts:Entity dictionary data to be added + :param model:Solid model class + ''' + + if model is None: + raise ContentNoneException('solid model must be specified') + + if dicts is None: + raise ContentNoneException( + 'The inserted data content cannot be empty') + + if not isinstance(dicts, list): + raise TypeError( + 'The input for bulk insertion must be a dictionary \ + list with the same fields as the current entity') + try: + self.session.execute( + model.__table__.insert(), + dicts + ) + except SQLAlchemyError as e: + raise Error(e) + else: + self.session.commit() diff --git a/packageship/packageship/libs/exception/__init__.py b/packageship/packageship/libs/exception/__init__.py new file mode 100644 index 00000000..5f13a795 --- /dev/null +++ b/packageship/packageship/libs/exception/__init__.py @@ -0,0 +1,8 @@ +from packageship.libs.exception.ext import ContentNoneException +from packageship.libs.exception.ext import DatabaseRepeatException +from packageship.libs.exception.ext import DataMergeException +from packageship.libs.exception.ext import Error +from packageship.libs.exception.ext import DbnameNoneException + +__all__ = ['ContentNoneException', + 'DatabaseRepeatException', 'DataMergeException', 'Error', 'DbnameNoneException'] diff --git a/packageship/packageship/libs/exception/ext.py b/packageship/packageship/libs/exception/ext.py new file mode 100644 index 00000000..b4ff3bdd --- /dev/null +++ b/packageship/packageship/libs/exception/ext.py @@ -0,0 +1,48 @@ + +class Error(Exception): + """Base class for ConfigParser exceptions.""" + + def __init__(self, msg=''): + self.message = msg + Exception.__init__(self, msg) + + def __repr__(self): + return self.message + + __str__ = __repr__ + + +class ContentNoneException(Error): + ''' + Content is empty exception + ''' + + def __init__(self, message): + Error.__init__(self, 'No content: %r' % (message,)) + + +class DbnameNoneException(ContentNoneException): + ''' + Exception with empty database name + ''' + + def __init__(self, message): + ContentNoneException.__init__(self, '%r' % (message,)) + + +class DatabaseRepeatException(Error): + ''' + There are duplicate exceptions in the database + ''' + + def __init__(self, message): + Error.__init__(self, 'Database repeat: %r' % (message,)) + + +class DataMergeException(Error): + ''' + abnormal integration data + ''' + + def __init__(self, message): + Error.__init__(self, 'DataMerge exception: %r' % (message,)) diff --git a/packageship/packageship/libs/log/__init__.py b/packageship/packageship/libs/log/__init__.py new file mode 100644 index 00000000..7b5fbf4d --- /dev/null +++ b/packageship/packageship/libs/log/__init__.py @@ -0,0 +1,4 @@ +from packageship.libs.log.loghelper import setup_log +from packageship.libs.log.loghelper import Log + +__all__ = ['setup_log', 'Log'] diff --git a/packageship/packageship/libs/log/loghelper.py b/packageship/packageship/libs/log/loghelper.py new file mode 100644 index 00000000..1e0fa8e1 --- /dev/null +++ b/packageship/packageship/libs/log/loghelper.py @@ -0,0 +1,93 @@ +''' + Configure log function +''' + +import os +import pathlib +import logging +from logging.handlers import RotatingFileHandler +from packageship.system_config import LOG_FOLDER_PATH +from packageship.libs.configutils.readconfig import ReadConfig + + +READCONFIG = ReadConfig() + + +def setup_log(Config=None): + + if Config: + + logging.basicConfig(level=Config.LOG_LEVEL) + else: + logging.basicConfig(level='INFO') + path = READCONFIG.get_system('log_path') + if path is None: + path = os.path.join(LOG_FOLDER_PATH, 'loginfo.log') + if not os.path.exists(path): + try: + os.makedirs(os.path.split(path)[0]) + except FileExistsError as file_exists: + pathlib.Path(path).touch() + + file_log_handler = RotatingFileHandler( + path, maxBytes=1024 * 1024 * 300, backupCount=10) + + formatter = logging.Formatter( + '%(levelname)s %(filename)s:%(lineno)d %(message)s') + + file_log_handler.setFormatter(formatter) + + logging.getLogger().addHandler(file_log_handler) + + +class Log(): + + def __init__(self, name=__name__, path=None, level='ERROR'): + self.__name = name + self.__path = path + if self.__path is None: + self.__path = READCONFIG.get_system('log_path') + if self.__path is None: + self.__path = os.path.join(LOG_FOLDER_PATH, 'loginfo.log') + else: + self.__path = os.path.join(LOG_FOLDER_PATH, path) + + if not os.path.exists(self.__path): + try: + os.makedirs(os.path.split(self.__path)[0]) + except FileExistsError: + pathlib.Path(self.__path).touch() + self.__level = level + self.__logger = logging.getLogger(self.__name) + self.__logger.setLevel(self.__level) + + def __ini_handler(self): + # self.__stream_handler = logging.StreamHandler() + self.__file_handler = logging.FileHandler( + self.__path, encoding='utf-8') + + def __set_handler(self, level='DEBUG'): + # self.__stream_handler.setLevel(level) + self.__file_handler.setLevel(level) + # self.__logger.addHandler(self.__stream_handler) + self.__logger.addHandler(self.__file_handler) + + def __set_formatter(self): + formatter = logging.Formatter('%(asctime)s-%(name)s-%(filename)s-[line:%(lineno)d]' + '-%(levelname)s-[ log details ]: %(message)s', + datefmt='%a, %d %b %Y %H:%M:%S') + # self.__stream_handler.setFormatter(formatter) + self.__file_handler.setFormatter(formatter) + + def __close_handler(self): + # self.__stream_handler.close() + self.__file_handler.close() + + @property + def logger(self): + + self.__ini_handler() + self.__set_handler() + self.__set_formatter() + self.__close_handler() + return self.__logger diff --git a/packageship/packageship/manage.ini b/packageship/packageship/manage.ini new file mode 100644 index 00000000..6a21d08a --- /dev/null +++ b/packageship/packageship/manage.ini @@ -0,0 +1,13 @@ +[uwsgi] + +http = 127.0.0.1:8080 + +module=packageship.manage + +wsgi-file=/usr/lib/python3.7/site-packages/packageship/manage.py + +callable=app + +buffer-size=65536 + +daemonize = /var/log/uwsgi.log diff --git a/packageship/packageship/manage.py b/packageship/packageship/manage.py new file mode 100644 index 00000000..cdfe9d40 --- /dev/null +++ b/packageship/packageship/manage.py @@ -0,0 +1,44 @@ +''' + Entry for project initialization and service startupc +''' +import os +from flask_script import Manager +from packageship.libs.exception import Error +try: + from packageship.system_config import SYS_CONFIG_PATH + if not os.path.exists(SYS_CONFIG_PATH): + raise FileNotFoundError( + 'the system configuration file does not exist and the log cannot be started') +except FileNotFoundError as file_not_found: + from packageship.libs.log.loghelper import Log + Log(__name__).logger.error(file_not_found) + raise Exception( + 'the system configuration file does not exist and the log cannot be started') +else: + from packageship.libs.configutils.readconfig import ReadConfig + from packageship.application.initsystem.data_import import InitDataBase + +from packageship.application import init_app +try: + app = init_app('prop', 'write') +except Error as error: + raise Exception('Service failed to start') +else: + from packageship.application.app_global import identity_verification + + +@app.before_request +def before_request(): + if not identity_verification(): + return 'No right to perform operation' + + +if __name__ == "__main__": + _readconfig = ReadConfig() + port = _readconfig.get_system('write_port') + if port is None: + port = 5000 + addr = _readconfig.get_system('write_ip_addr') + if addr is None: + addr = '127.0.0.1' + app.run(port=port, host=addr) diff --git a/packageship/packageship/package.ini b/packageship/packageship/package.ini new file mode 100644 index 00000000..e578ad60 --- /dev/null +++ b/packageship/packageship/package.ini @@ -0,0 +1,60 @@ +[SYSTEM] + +; Configuration file path for data initialization +init_conf_path=/etc/pkgship/conf.yaml + +; Custom log storage path +; log_path=/run/log + +; Where to store data files when using sqlite database +; data_base_path=/var/run/pkgship_dbs + +; Port managed by the administrator, with write permission + +write_port=8080 + +; Ordinary user query port, only the right to query data, no permission to write data + +query_port=8090 + +; IP address path with write permission + +write_ip_addr=127.0.0.1 + +; IP address path with permission to query data + +query_ip_addr=127.0.0.1 + + +[DATABASE] + +; Basic configuration of sqlalchemy to connect to the database + +; user_name: Username of the database + +; password: connection password + +; host: host address + +; port: number for database connection + +; database:Connected data name + +; dbtype:The type of database is mainly divided into mysql and sqlite + +user_name= + +password= + +host= + +port= + +database= + +dbtype=sqlite + + + + + diff --git a/packageship/packageship/selfpkg.ini b/packageship/packageship/selfpkg.ini new file mode 100644 index 00000000..c15eebc2 --- /dev/null +++ b/packageship/packageship/selfpkg.ini @@ -0,0 +1,13 @@ +[uwsgi] + +http = 127.0.0.1:8090 + +module=packageship.selfpkg + +wsgi-file=/usr/lib/python3.7/site-packages/packageship/selfpkg.py + +callable=app + +buffer-size=65536 + +daemonize = /var/log/uwsgi.log diff --git a/packageship/packageship/selfpkg.py b/packageship/packageship/selfpkg.py new file mode 100644 index 00000000..b4139439 --- /dev/null +++ b/packageship/packageship/selfpkg.py @@ -0,0 +1,43 @@ +''' + Entry for project initialization and service startupc +''' +import os +from flask_script import Manager +from packageship.libs.exception import Error +from packageship.libs.configutils.readconfig import ReadConfig + +try: + from packageship.system_config import SYS_CONFIG_PATH + if not os.path.exists(SYS_CONFIG_PATH): + raise FileNotFoundError( + 'the system configuration file does not exist and the log cannot be started') +except FileNotFoundError as file_not_found: + from packageship.libs.log.loghelper import Log + Log(__name__).logger.error(file_not_found) + raise Exception( + 'the system configuration file does not exist and the log cannot be started') + +from packageship.application import init_app +try: + app = init_app('prop', 'query') +except Error as error: + raise Exception('Service failed to start') +else: + from packageship.application.app_global import identity_verification + + +@app.before_request +def before_request(): + if not identity_verification(): + return 'No right to perform operation' + + +if __name__ == "__main__": + _readconfig = ReadConfig() + port = _readconfig.get_system('query_port') + if port is None: + port = 5000 + addr = _readconfig.get_system('query_ip_addr') + if addr is None: + addr = '127.0.0.1' + app.run(port=port, host=addr) diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py new file mode 100644 index 00000000..6ee7da5a --- /dev/null +++ b/packageship/packageship/system_config.py @@ -0,0 +1,35 @@ +''' +System-level file configuration, mainly configure +the address of the operating environment, commonly used variables, etc. +''' + +import os +import sys + + +# The root directory where the system is running +if getattr(sys, 'frozen', False): + BASE_PATH = os.path.dirname(os.path.realpath(sys.argv[0])) +else: + BASE_PATH = os.path.abspath(os.path.dirname(__file__)) + +# system configuration file path + +SYS_CONFIG_PATH = os.path.join('/', 'etc', 'pkgship', 'package.ini') + +# data file after successful data import + +# DATABASE_SUCCESS_FILE = os.path.join( +# BASE_PATH, 'application', 'initsystem', 'import_success_databse.yaml') + +DATABASE_SUCCESS_FILE = os.path.join( + '/', 'var', 'run', 'import_success_databse.yaml') + +# If the path of the imported database is not specified in the configuration file, the +# configuration in the system is used by default +DATABASE_FOLDER_PATH = os.path.join('/', 'var', 'run', 'pkgship_dbs') + + +# If the directory of log storage is not configured, +# it will be stored in the following directory specified by the system by default +LOG_FOLDER_PATH = os.path.join('/', 'run', 'log') diff --git a/packageship/setup.py b/packageship/setup.py new file mode 100644 index 00000000..3183e294 --- /dev/null +++ b/packageship/setup.py @@ -0,0 +1,73 @@ +''' +include modules and requirement +''' +from distutils.core import setup +import os + +BASE_PATH = os.path.dirname(__file__) + +path = os.path.join(BASE_PATH, 'Lib', 'site-packages', 'package') + +configpath = "/etc/pkgship/" + +setup( + name='packageship', + version='1.0', + py_modules=[ + 'packageship.application.__init__', + 'packageship.application.app_global', + 'packageship.application.apps.__init__', + 'packageship.application.apps.package.serialize', + 'packageship.application.apps.package.url', + 'packageship.application.apps.package.view', + 'packageship.application.apps.package.function.BeDepend', + 'packageship.application.apps.package.function.BuildDepend', + 'packageship.application.apps.package.function.Constants', + 'packageship.application.apps.package.function.InstallDepend', + 'packageship.application.apps.package.function.Packages', + 'packageship.application.apps.package.function.SearchDB', + 'packageship.application.apps.package.function.SelfDepend', + 'packageship.application.initsystem.data_import', + 'packageship.application.initsystem.datamerge', + 'packageship.application.models.package', + 'packageship.application.models.temporarydb', + 'packageship.application.settings.dev', + 'packageship.application.settings.pro', + 'packageship.libs.__init__', + 'packageship.libs.configutils.readconfig', + 'packageship.libs.dbutils.sqlalchemy_helper', + 'packageship.libs.dbutils.sqlalchemy_config', + 'packageship.libs.exception.ext', + 'packageship.libs.log.loghelper', + 'packageship.tests.test_build_depend', + 'packageship.tests.test_delete_repodatas', + 'packageship.tests.test_get_repodatas', + 'packageship.tests.test_get_singlepack', + 'packageship.tests.test_install_depend', + 'packageship.tests.test_packages', + 'packageship.tests.test_self_depend', + 'packageship.tests.test_update_singlepack', + 'packageship.tests.test_importdata', + 'packageship.manage', + 'packageship.cli', + 'packageship.selfpkg', + 'packageship.system_config'], + requires=['prettytable (==0.7.2)', + 'Flask_RESTful (==0.3.8)', + 'Flask_Session (==0.3.1)', + 'Flask_Script (==2.0.6)', + 'Flask (==1.1.2)', + 'marshmallow (==3.5.1)', + 'SQLAlchemy (==1.3.16)', + 'PyYAML (==5.3.1)', + 'requests (==2.21.0)', + 'pyinstall (==0.1.4)', + 'uwsgi (==2.0.18)'], + license='Dependency package management', + long_description=open('README.md', encoding='utf-8').read(), + author='gongzt', + data_files=[ + (configpath, ['packageship/package.ini', + 'packageship/manage.ini', + 'packageship/selfpkg.ini'])] +) -- Gitee From ba9ed582400a49105c6c7e114c22679acffadc4e Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Sat, 20 Jun 2020 11:30:18 +0800 Subject: [PATCH 03/72] remove unused import InintDataBase --- packageship/packageship/manage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packageship/packageship/manage.py b/packageship/packageship/manage.py index cdfe9d40..e6b172a6 100644 --- a/packageship/packageship/manage.py +++ b/packageship/packageship/manage.py @@ -16,7 +16,6 @@ except FileNotFoundError as file_not_found: 'the system configuration file does not exist and the log cannot be started') else: from packageship.libs.configutils.readconfig import ReadConfig - from packageship.application.initsystem.data_import import InitDataBase from packageship.application import init_app try: -- Gitee From 16e5e101d66912183791edc5a68012f27f5750ff Mon Sep 17 00:00:00 2001 From: "solar.hu" Date: Sat, 20 Jun 2020 14:27:22 +0800 Subject: [PATCH 04/72] init pkgship:add spec file --- packageship/pkgship.spec | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packageship/pkgship.spec diff --git a/packageship/pkgship.spec b/packageship/pkgship.spec new file mode 100644 index 00000000..d2748489 --- /dev/null +++ b/packageship/pkgship.spec @@ -0,0 +1,59 @@ +Name: pkgship +Version: 1.0 +Release: 0 +Summary: Pkgship implements rpm package dependence ,maintainer, patch query and so no. +License: Mulan 2.0 +URL: https://gitee.com/openeuler/openEuler-Advisor +Source0: https://gitee.com/openeuler/openEuler-Advisor/pkgship-%{version}.tar + +BuildArch: noarch + +Requires: python3-pip python3-flask-restful python3-flask python3.7 python3-pyyaml +Requires: python3-sqlalchemy python3-prettytable python3-requests +#Requires: pyinstaller python3-flask-session python3-flask-script marshmallow uwsig + +%description +Pkgship implements rpm package dependence ,maintainer, patch query and so no. + +%prep +%autosetup -n pkgship-%{version} + +%build +%py3_build + +%install +%py3_install + + +%check + + +%post +#build cli bin +if [ -f "/usr/bin/cli" ];then + rm -rf /usr/bin/cli +fi + + +cd %{python3_sitelib}/packageship/ +/usr/local/bin/pyinstaller -F cli.py +sed -i "s/hiddenimports\=\[\]/hiddenimports\=\['pkg_resources.py2_warn'\]/g" cli.spec +/usr/local/bin/pyinstaller cli.spec +cp dist/cli /usr/bin/ +rm -rf %{python3_sitelib}/packageship/build %{python3_sitelib}/packageship/dist + +%postun + + +%files +%doc README.md +%{python3_sitelib}/* +%config %{_sysconfdir}/pkgship/* + + +%changelog +* Tue Jun 11 2020 Feng Hu +- add macro to build cli bin when rpm install + +* Sat Jun 6 2020 Feng Hu +- init package -- Gitee From b07c295e74f77f0331568148c0529b3dd1d4c415 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Sat, 20 Jun 2020 16:59:45 +0800 Subject: [PATCH 05/72] Randomly generated SECRET KEY, Configurable logs, services, etc --- .../packageship/application/__init__.py | 19 ++---- .../packageship/application/setting.py | 59 +++++++++++++++++++ .../application/setting/__init__.py | 14 ----- .../packageship/application/setting/dev.py | 9 --- .../packageship/application/setting/pro.py | 9 --- packageship/packageship/libs/log/loghelper.py | 38 +++++++----- packageship/packageship/manage.ini | 10 ++++ packageship/packageship/manage.py | 6 +- packageship/packageship/package.ini | 37 +++++++----- packageship/packageship/selfpkg.ini | 10 ++++ packageship/packageship/selfpkg.py | 6 +- packageship/packageship/system_config.py | 2 +- packageship/setup.py | 10 +--- 13 files changed, 134 insertions(+), 95 deletions(-) create mode 100644 packageship/packageship/application/setting.py delete mode 100644 packageship/packageship/application/setting/__init__.py delete mode 100644 packageship/packageship/application/setting/dev.py delete mode 100644 packageship/packageship/application/setting/pro.py diff --git a/packageship/packageship/application/__init__.py b/packageship/packageship/application/__init__.py index 89bcd028..e991c560 100644 --- a/packageship/packageship/application/__init__.py +++ b/packageship/packageship/application/__init__.py @@ -3,35 +3,24 @@ ''' from flask import Flask from flask_session import Session -from packageship.application.settings.dev import DevelopementConfig -from packageship.application.settings.pro import ProductionConfig +from packageship.application.settings import Config from packageship.libs.log import setup_log - -# development and production environment configuration - -CONFIG = { - 'dev': DevelopementConfig, - 'prop': ProductionConfig -} - OPERATION = None -def init_app(config_name, operation): +def init_app(operation): ''' Project initialization function ''' app = Flask(__name__) - config = CONFIG[config_name] - # log configuration - setup_log(config) + setup_log(Config) # Load configuration items - app.config.from_object(config) + app.config.from_object(Config) # Open session function Session(app) diff --git a/packageship/packageship/application/setting.py b/packageship/packageship/application/setting.py new file mode 100644 index 00000000..743d3db8 --- /dev/null +++ b/packageship/packageship/application/setting.py @@ -0,0 +1,59 @@ +from packageship.libs.configutils.readconfig import ReadConfig +import random +import string + + +class Config(): + ''' + Configuration items in a formal environment + ''' + SECRET_KEY = None + + DEBUG = False + + LOG_LEVEL = 'INFO' + + def __init__(self, *args, **kwargs): + + self._read_config = ReadConfig() + + self.set_config_val() + + @classmethod + def _random_secret_key(cls, random_len=32): + ''' + Generate random strings + ''' + cls.SECRET_KEY = ''.join( + [random.choice('abcdefghijklmnopqrstuvwxyz!@#$%^&*()') for index in range(random_len)]) + + @classmethod + def _set_debug(cls, debug): + ''' + Set the debugging mode + ''' + if debug == 'true': + cls.DEBUG = True + + @classmethod + def _set_log_level(cls, log_level): + ''' + Set the log level + ''' + cls.LOG_LEVEL = log_level + + def set_config_val(self): + ''' + Set the value of the configuration item + ''' + Config._random_secret_key() + + debug = self._read_config.get_system('debug') + + if debug: + Config._set_debug(debug) + + log_level = self._read_config.get_config('LOG', 'log_level') + + if log_level: + Config._set_log_level(log_level) diff --git a/packageship/packageship/application/setting/__init__.py b/packageship/packageship/application/setting/__init__.py deleted file mode 100644 index 1d03095c..00000000 --- a/packageship/packageship/application/setting/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ - - -class Config(object): - ''' - Basic configuration items of the project - ''' - - # Debug mode - - DEBUG = True - - LOG_LEVEL = "DEBUG" - - SECRET_KEY = 'serwj1232aeaeq@adssr3rssa3123dvfdge53ad' diff --git a/packageship/packageship/application/setting/dev.py b/packageship/packageship/application/setting/dev.py deleted file mode 100644 index 73c6087a..00000000 --- a/packageship/packageship/application/setting/dev.py +++ /dev/null @@ -1,9 +0,0 @@ -from . import Config - - -class DevelopementConfig(Config): - ''' - Configuration in development mode - ''' - - DEBUG = True diff --git a/packageship/packageship/application/setting/pro.py b/packageship/packageship/application/setting/pro.py deleted file mode 100644 index 121ec5fb..00000000 --- a/packageship/packageship/application/setting/pro.py +++ /dev/null @@ -1,9 +0,0 @@ -from . import Config - - -class ProductionConfig(Config): - ''' - Configuration in development mode - ''' - - DEBUG = False diff --git a/packageship/packageship/libs/log/loghelper.py b/packageship/packageship/libs/log/loghelper.py index 1e0fa8e1..b4b90c3f 100644 --- a/packageship/packageship/libs/log/loghelper.py +++ b/packageship/packageship/libs/log/loghelper.py @@ -1,7 +1,3 @@ -''' - Configure log function -''' - import os import pathlib import logging @@ -9,20 +5,26 @@ from logging.handlers import RotatingFileHandler from packageship.system_config import LOG_FOLDER_PATH from packageship.libs.configutils.readconfig import ReadConfig - READCONFIG = ReadConfig() def setup_log(Config=None): - + ''' + Log logging in the context of flask + ''' if Config: - logging.basicConfig(level=Config.LOG_LEVEL) else: - logging.basicConfig(level='INFO') - path = READCONFIG.get_system('log_path') + _level = READCONFIG.get_config('LOG', 'log_level') + if _level is None: + _level = 'INFO' + logging.basicConfig(level=_level) + path = READCONFIG.get_config('LOG', 'log_path') if path is None: - path = os.path.join(LOG_FOLDER_PATH, 'loginfo.log') + log_name = READCONFIG.get_config('LOG', 'log_name') + if log_name is None: + log_name = 'log_info.log' + path = os.path.join(LOG_FOLDER_PATH, log_name) if not os.path.exists(path): try: os.makedirs(os.path.split(path)[0]) @@ -42,13 +44,16 @@ def setup_log(Config=None): class Log(): - def __init__(self, name=__name__, path=None, level='ERROR'): + def __init__(self, name=__name__, path=None): self.__name = name self.__path = path if self.__path is None: self.__path = READCONFIG.get_system('log_path') + log_name = READCONFIG.get_config('LOG', 'log_name') + if log_name is None: + log_name = 'log_info.log' if self.__path is None: - self.__path = os.path.join(LOG_FOLDER_PATH, 'loginfo.log') + self.__path = os.path.join(LOG_FOLDER_PATH, log_name) else: self.__path = os.path.join(LOG_FOLDER_PATH, path) @@ -57,7 +62,9 @@ class Log(): os.makedirs(os.path.split(self.__path)[0]) except FileExistsError: pathlib.Path(self.__path).touch() - self.__level = level + self.__level = READCONFIG.get_config('LOG', 'log_level') + if self.__level is None: + self.__level = 'INFO' self.__logger = logging.getLogger(self.__name) self.__logger.setLevel(self.__level) @@ -66,9 +73,9 @@ class Log(): self.__file_handler = logging.FileHandler( self.__path, encoding='utf-8') - def __set_handler(self, level='DEBUG'): + def __set_handler(self): # self.__stream_handler.setLevel(level) - self.__file_handler.setLevel(level) + self.__file_handler.setLevel(self.__level) # self.__logger.addHandler(self.__stream_handler) self.__logger.addHandler(self.__file_handler) @@ -85,7 +92,6 @@ class Log(): @property def logger(self): - self.__ini_handler() self.__set_handler() self.__set_formatter() diff --git a/packageship/packageship/manage.ini b/packageship/packageship/manage.ini index 6a21d08a..07250260 100644 --- a/packageship/packageship/manage.ini +++ b/packageship/packageship/manage.ini @@ -1,13 +1,23 @@ [uwsgi] +; add an http router/server on the specified address http = 127.0.0.1:8080 +;load a WSGI module module=packageship.manage +;load .wsgi file wsgi-file=/usr/lib/python3.7/site-packages/packageship/manage.py +;set default WSGI callable name callable=app +;Set internal buffer size +;Set the max size of a request (request-body excluded), +;this generally maps to the size of request headers. +;It is a security measure too, so adapt to your app +;needs instead of maxing it out. buffer-size=65536 +;daemonize uWSGI daemonize = /var/log/uwsgi.log diff --git a/packageship/packageship/manage.py b/packageship/packageship/manage.py index e6b172a6..d314ea68 100644 --- a/packageship/packageship/manage.py +++ b/packageship/packageship/manage.py @@ -19,7 +19,7 @@ else: from packageship.application import init_app try: - app = init_app('prop', 'write') + app = init_app('write') except Error as error: raise Exception('Service failed to start') else: @@ -35,9 +35,5 @@ def before_request(): if __name__ == "__main__": _readconfig = ReadConfig() port = _readconfig.get_system('write_port') - if port is None: - port = 5000 addr = _readconfig.get_system('write_ip_addr') - if addr is None: - addr = '127.0.0.1' app.run(port=port, host=addr) diff --git a/packageship/packageship/package.ini b/packageship/packageship/package.ini index e578ad60..fe0602da 100644 --- a/packageship/packageship/package.ini +++ b/packageship/packageship/package.ini @@ -3,8 +3,8 @@ ; Configuration file path for data initialization init_conf_path=/etc/pkgship/conf.yaml -; Custom log storage path -; log_path=/run/log +; Whether the system is in debug mode +debug=false ; Where to store data files when using sqlite database ; data_base_path=/var/run/pkgship_dbs @@ -30,31 +30,40 @@ query_ip_addr=127.0.0.1 ; Basic configuration of sqlalchemy to connect to the database -; user_name: Username of the database - -; password: connection password - -; host: host address - -; port: number for database connection - -; database:Connected data name - -; dbtype:The type of database is mainly divided into mysql and sqlite - +;Username of the database user_name= +;connection password password= +;host address host= +; number for database connection port= +;Connected data name database= +; dbtype:The type of database is mainly divided into mysql and sqlite dbtype=sqlite +[LOG] + +; Custom log storage path +; log_path=/var/run/pkgship + +; Logging level +; The log level option value can only be as follows +; INFO DEBUG WARNING ERROR CRITICAL +log_level=INFO + +; logging name +log_name=log_info.log +[UWSGI] +daemonize=/var/run/pkgship/uwsgi.log +buffer-size=65536 diff --git a/packageship/packageship/selfpkg.ini b/packageship/packageship/selfpkg.ini index c15eebc2..ec6c197e 100644 --- a/packageship/packageship/selfpkg.ini +++ b/packageship/packageship/selfpkg.ini @@ -1,13 +1,23 @@ [uwsgi] +; add an http router/server on the specified address http = 127.0.0.1:8090 +;load a WSGI module module=packageship.selfpkg +;load .wsgi file wsgi-file=/usr/lib/python3.7/site-packages/packageship/selfpkg.py +;set default WSGI callable name callable=app +;Set internal buffer size +;Set the max size of a request (request-body excluded), +;this generally maps to the size of request headers. +;It is a security measure too, so adapt to your app +;needs instead of maxing it out. buffer-size=65536 +;daemonize uWSGI daemonize = /var/log/uwsgi.log diff --git a/packageship/packageship/selfpkg.py b/packageship/packageship/selfpkg.py index b4139439..31d9add2 100644 --- a/packageship/packageship/selfpkg.py +++ b/packageship/packageship/selfpkg.py @@ -19,7 +19,7 @@ except FileNotFoundError as file_not_found: from packageship.application import init_app try: - app = init_app('prop', 'query') + app = init_app('query') except Error as error: raise Exception('Service failed to start') else: @@ -35,9 +35,5 @@ def before_request(): if __name__ == "__main__": _readconfig = ReadConfig() port = _readconfig.get_system('query_port') - if port is None: - port = 5000 addr = _readconfig.get_system('query_ip_addr') - if addr is None: - addr = '127.0.0.1' app.run(port=port, host=addr) diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py index 6ee7da5a..b26d7598 100644 --- a/packageship/packageship/system_config.py +++ b/packageship/packageship/system_config.py @@ -32,4 +32,4 @@ DATABASE_FOLDER_PATH = os.path.join('/', 'var', 'run', 'pkgship_dbs') # If the directory of log storage is not configured, # it will be stored in the following directory specified by the system by default -LOG_FOLDER_PATH = os.path.join('/', 'run', 'log') +LOG_FOLDER_PATH = os.path.join('/', 'var', 'log', 'pkgship') diff --git a/packageship/setup.py b/packageship/setup.py index 3183e294..c1f34a66 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -1,6 +1,3 @@ -''' -include modules and requirement -''' from distutils.core import setup import os @@ -31,12 +28,10 @@ setup( 'packageship.application.initsystem.datamerge', 'packageship.application.models.package', 'packageship.application.models.temporarydb', - 'packageship.application.settings.dev', - 'packageship.application.settings.pro', + 'packageship.application.settings', 'packageship.libs.__init__', 'packageship.libs.configutils.readconfig', 'packageship.libs.dbutils.sqlalchemy_helper', - 'packageship.libs.dbutils.sqlalchemy_config', 'packageship.libs.exception.ext', 'packageship.libs.log.loghelper', 'packageship.tests.test_build_depend', @@ -69,5 +64,6 @@ setup( data_files=[ (configpath, ['packageship/package.ini', 'packageship/manage.ini', - 'packageship/selfpkg.ini'])] + 'packageship/selfpkg.ini', + 'packageship/packageship.sh'])] ) -- Gitee From a9c0aed8f351811266ff5259b634322af9fabd0b Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Sun, 21 Jun 2020 20:48:23 +0800 Subject: [PATCH 06/72] modify uwsgi start service, remove manage.ini and selfpkg.ini config file, and change the start service method to run the uWSGI_service.sh file --- packageship/README.md | 15 +++- packageship/packageship/manage.ini | 23 ------ packageship/packageship/selfpkg.ini | 23 ------ packageship/packageship/uWSGI_service.sh | 93 ++++++++++++++++++++++++ packageship/setup.py | 3 +- 5 files changed, 107 insertions(+), 50 deletions(-) delete mode 100644 packageship/packageship/manage.ini delete mode 100644 packageship/packageship/selfpkg.ini create mode 100644 packageship/packageship/uWSGI_service.sh diff --git a/packageship/README.md b/packageship/README.md index 1639cd21..59562c27 100644 --- a/packageship/README.md +++ b/packageship/README.md @@ -56,9 +56,20 @@ pkgmnt希望提供软件包依赖,生命周期,补丁查询等功能。 3.3 启动系统服务 - uwsgi -d --ini manage.ini + 单独启动manage服务: sh /etc/pkgship/uWSGI_service.sh start manage + + 单独启动selfpkg服务: sh /etc/pkgship/uWSGI_service.sh start selfpkg + + 同时启动manage和selfpkg服务: sh /etc/pkgship/uWSGI_service.sh start + + 3.4 停止系统服务 + 停止manage服务: sh /etc/pkgship/uWSGI_service.sh stop manage + + 停止selfpkg服务: sh /etc/pkgship/uWSGI_service.sh stop selfpkg + + 同时停止manage和selfpkg服务: sh /etc/pkgship/uWSGI_service.sh stop + - uwsgi -d --ini selfpkg.ini #### 使用说明 diff --git a/packageship/packageship/manage.ini b/packageship/packageship/manage.ini deleted file mode 100644 index 07250260..00000000 --- a/packageship/packageship/manage.ini +++ /dev/null @@ -1,23 +0,0 @@ -[uwsgi] - -; add an http router/server on the specified address -http = 127.0.0.1:8080 - -;load a WSGI module -module=packageship.manage - -;load .wsgi file -wsgi-file=/usr/lib/python3.7/site-packages/packageship/manage.py - -;set default WSGI callable name -callable=app - -;Set internal buffer size -;Set the max size of a request (request-body excluded), -;this generally maps to the size of request headers. -;It is a security measure too, so adapt to your app -;needs instead of maxing it out. -buffer-size=65536 - -;daemonize uWSGI -daemonize = /var/log/uwsgi.log diff --git a/packageship/packageship/selfpkg.ini b/packageship/packageship/selfpkg.ini deleted file mode 100644 index ec6c197e..00000000 --- a/packageship/packageship/selfpkg.ini +++ /dev/null @@ -1,23 +0,0 @@ -[uwsgi] - -; add an http router/server on the specified address -http = 127.0.0.1:8090 - -;load a WSGI module -module=packageship.selfpkg - -;load .wsgi file -wsgi-file=/usr/lib/python3.7/site-packages/packageship/selfpkg.py - -;set default WSGI callable name -callable=app - -;Set internal buffer size -;Set the max size of a request (request-body excluded), -;this generally maps to the size of request headers. -;It is a security measure too, so adapt to your app -;needs instead of maxing it out. -buffer-size=65536 - -;daemonize uWSGI -daemonize = /var/log/uwsgi.log diff --git a/packageship/packageship/uWSGI_service.sh b/packageship/packageship/uWSGI_service.sh new file mode 100644 index 00000000..28b55870 --- /dev/null +++ b/packageship/packageship/uWSGI_service.sh @@ -0,0 +1,93 @@ +#!/bin/bash +SYS_PATH=/etc/pkgship/ + +function get_config(){ + cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' > config_file +} + +function create_config_file(){ + echo "config type is: $config_type" + get_config "$config_type" "daemonize" + daemonize=$(cat config_file) + get_config "$config_type" "buffer-size" + buffer_size=$(cat config_file) + wsgi_file_path=$(find /usr/lib/ -name "packageship") + if [ $config_type = "manage" -o $config_type = "all" ];then + get_config "$config_type" "write_port" + write_port=$(cat config_file) + get_config "$config_type" "write_ip_addr" + write_ip_addr=$(cat config_file) + echo "manage.ini:" + echo "[uwsgi] +http=$write_ip_addr:$write_port +module=packageship.manage +wsgi-file=$wsgi_file_path/manage.py +callable=app +buffer-size=$buffer_size +daemonize=$daemonize" > manage.ini + cat manage.ini + fi + + if [ $config_type = "selfpkg" -o $config_type = "all" ];then + get_config "$config_type" "query_port" + query_port=$(cat config_file) + get_config "$config_type" "query_ip_addr" + query_ip_addr=$(cat config_file) + echo "selfpkg.ini:" + echo "[uwsgi] +http=$query_ip_addr:$query_port +module=packageship.selfpkg +wsgi-file=$wsgi_file_path/selfpkg.py +callable=app +buffer-size=$buffer_size +daemonize=$daemonize" > selfpkg.ini + cat selfpkg.ini + + fi + +} + +if [ ! -n "$1" ] +then + echo "Usages: sh packageship.sh [start|stop|restart]" + exit 0 +fi + +if [ X$2 = X ];then + config_type="all" +elif [ $2 = "manage" -o $2 = "selfpkg" ];then + config_type=$2 +else + echo "can not phase the input of $2" + exit 0 +fi + +create_config_file $config_type + +if [ $1 = start ] +then + psid=`ps aux | grep "uwsgi" | grep -v "grep" | wc -l` + if [ $psid -gt 4 ] + then + echo "uwsgi is running!" + exit 0 + else + uwsgi -d --ini /etc/pkgship/manage.ini + uwsgi -d --ini /etc/pkgship/selfpkg.ini + echo "Start uwsgi service [OK]" + fi + + +elif [ $1 = stop ];then + killall -9 uwsgi + echo "Stop uwsgi service [OK]" +elif [ $1 = restart ];then + killall -9 uwsgi + uwsgi -d --ini /etc/pkgship/manage.ini + uwsgi -d --ini /etc/pkgship/selfpkg.ini + echo "Restart uwsgi service [OK]" + +else + echo "Usages: sh packageship.sh [start|stop|restart]" +fi + diff --git a/packageship/setup.py b/packageship/setup.py index c1f34a66..61efa02e 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -63,7 +63,6 @@ setup( author='gongzt', data_files=[ (configpath, ['packageship/package.ini', - 'packageship/manage.ini', - 'packageship/selfpkg.ini', + 'packageship/uWSGI_service.sh', 'packageship/packageship.sh'])] ) -- Gitee From af11f528e2db5399b16e5541140abd9c1da221f4 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 00:15:50 +0800 Subject: [PATCH 07/72] change the config files to absolute path --- packageship/packageship/uWSGI_service.sh | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packageship/packageship/uWSGI_service.sh b/packageship/packageship/uWSGI_service.sh index 28b55870..4cb2d661 100644 --- a/packageship/packageship/uWSGI_service.sh +++ b/packageship/packageship/uWSGI_service.sh @@ -24,8 +24,8 @@ module=packageship.manage wsgi-file=$wsgi_file_path/manage.py callable=app buffer-size=$buffer_size -daemonize=$daemonize" > manage.ini - cat manage.ini +daemonize=$daemonize" > $SYS_PATH/manage.ini + cat $SYS_PATH/manage.ini fi if [ $config_type = "selfpkg" -o $config_type = "all" ];then @@ -40,11 +40,12 @@ module=packageship.selfpkg wsgi-file=$wsgi_file_path/selfpkg.py callable=app buffer-size=$buffer_size -daemonize=$daemonize" > selfpkg.ini - cat selfpkg.ini +daemonize=$daemonize" > $SYS_PATH/selfpkg.ini + cat $SYS_PATH/selfpkg.ini fi + rm -f config_file } if [ ! -n "$1" ] @@ -74,18 +75,18 @@ then else uwsgi -d --ini /etc/pkgship/manage.ini uwsgi -d --ini /etc/pkgship/selfpkg.ini - echo "Start uwsgi service [OK]" + echo "Start uwsgi service [OK], Please see the run log in $daemonize" fi elif [ $1 = stop ];then killall -9 uwsgi - echo "Stop uwsgi service [OK]" + echo "Stop uwsgi service [OK], Please see the run log in $daemonize" elif [ $1 = restart ];then killall -9 uwsgi uwsgi -d --ini /etc/pkgship/manage.ini uwsgi -d --ini /etc/pkgship/selfpkg.ini - echo "Restart uwsgi service [OK]" + echo "Restart uwsgi service [OK], Please see the run log in $daemonize" else echo "Usages: sh packageship.sh [start|stop|restart]" -- Gitee From cfeea20e06bb877ecbf83be0d3eafda490b9b789 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 11:03:49 +0800 Subject: [PATCH 08/72] change service file path to /run/pkgship_uwsgi --- packageship/packageship/uWSGI_service.sh | 44 +++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packageship/packageship/uWSGI_service.sh b/packageship/packageship/uWSGI_service.sh index 4cb2d661..d5274442 100644 --- a/packageship/packageship/uWSGI_service.sh +++ b/packageship/packageship/uWSGI_service.sh @@ -1,21 +1,25 @@ #!/bin/bash SYS_PATH=/etc/pkgship/ +OUT_PATH=/run/pkgship_uwsgi +if [ ! -d "$OUT_PATH" ]; then + mkdir $OUT_PATH +fi function get_config(){ cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' > config_file } function create_config_file(){ - echo "config type is: $config_type" - get_config "$config_type" "daemonize" + echo "config type is: $service" + get_config "$service" "daemonize" daemonize=$(cat config_file) - get_config "$config_type" "buffer-size" + get_config "$service" "buffer-size" buffer_size=$(cat config_file) wsgi_file_path=$(find /usr/lib/ -name "packageship") - if [ $config_type = "manage" -o $config_type = "all" ];then - get_config "$config_type" "write_port" + if [ $service = "manage" -o $service = "all" ];then + get_config "$service" "write_port" write_port=$(cat config_file) - get_config "$config_type" "write_ip_addr" + get_config "$service" "write_ip_addr" write_ip_addr=$(cat config_file) echo "manage.ini:" echo "[uwsgi] @@ -24,14 +28,14 @@ module=packageship.manage wsgi-file=$wsgi_file_path/manage.py callable=app buffer-size=$buffer_size -daemonize=$daemonize" > $SYS_PATH/manage.ini - cat $SYS_PATH/manage.ini +daemonize=$daemonize" > $OUT_PATH/manage.ini + cat $OUT_PATH/manage.ini fi - if [ $config_type = "selfpkg" -o $config_type = "all" ];then - get_config "$config_type" "query_port" + if [ $service = "selfpkg" -o $service = "all" ];then + get_config "$service" "query_port" query_port=$(cat config_file) - get_config "$config_type" "query_ip_addr" + get_config "$service" "query_ip_addr" query_ip_addr=$(cat config_file) echo "selfpkg.ini:" echo "[uwsgi] @@ -40,8 +44,8 @@ module=packageship.selfpkg wsgi-file=$wsgi_file_path/selfpkg.py callable=app buffer-size=$buffer_size -daemonize=$daemonize" > $SYS_PATH/selfpkg.ini - cat $SYS_PATH/selfpkg.ini +daemonize=$daemonize" > $OUT_PATH/selfpkg.ini + cat $OUT_PATH/selfpkg.ini fi @@ -55,15 +59,15 @@ then fi if [ X$2 = X ];then - config_type="all" + service="all" elif [ $2 = "manage" -o $2 = "selfpkg" ];then - config_type=$2 + service=$2 else echo "can not phase the input of $2" exit 0 fi -create_config_file $config_type +create_config_file $service if [ $1 = start ] then @@ -73,8 +77,8 @@ then echo "uwsgi is running!" exit 0 else - uwsgi -d --ini /etc/pkgship/manage.ini - uwsgi -d --ini /etc/pkgship/selfpkg.ini + uwsgi -d --ini $OUT_PATH/manage.ini + uwsgi -d --ini $OUT_PATH/selfpkg.ini echo "Start uwsgi service [OK], Please see the run log in $daemonize" fi @@ -84,8 +88,8 @@ elif [ $1 = stop ];then echo "Stop uwsgi service [OK], Please see the run log in $daemonize" elif [ $1 = restart ];then killall -9 uwsgi - uwsgi -d --ini /etc/pkgship/manage.ini - uwsgi -d --ini /etc/pkgship/selfpkg.ini + uwsgi -d --ini $OUT_PATH/manage.ini + uwsgi -d --ini $OUT_PATH/selfpkg.ini echo "Restart uwsgi service [OK], Please see the run log in $daemonize" else -- Gitee From 26c590df100b0b660da6f617eb2a67c9476123bc Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 17:48:08 +0800 Subject: [PATCH 09/72] modify killall operation --- packageship/packageship/pkgshipd.sh | 114 ++++++++++++++++++++++++++++ packageship/setup.py | 5 +- 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 packageship/packageship/pkgshipd.sh diff --git a/packageship/packageship/pkgshipd.sh b/packageship/packageship/pkgshipd.sh new file mode 100644 index 00000000..c5475ad1 --- /dev/null +++ b/packageship/packageship/pkgshipd.sh @@ -0,0 +1,114 @@ +#!/bin/bash +SYS_PATH=/etc/pkgship/ +OUT_PATH=/var/run/pkgship_uwsgi +if [ ! -d "$OUT_PATH" ]; then + mkdir $OUT_PATH +fi + +function get_config(){ + cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' +} + +function create_config_file(){ + echo "config type is: $service" + daemonize=$(get_config "$service" "daemonize") + buffer_size=$(get_config "$service" "buffer-size") + wsgi_file_path=$(find /usr/lib/ -name "packageship") + if [ $service = "manage" -o $service = "all" ];then + write_port=$(get_config "$service" "write_port") + write_ip_addr=$(get_config "$service" "write_ip_addr") + echo "manage.ini is saved to $OUT_PATH/manage.ini" + echo "[uwsgi] +http=$write_ip_addr:$write_port +module=packageship.manage +wsgi-file=$wsgi_file_path/manage.py +callable=app +buffer-size=$buffer_size +pidfile=$OUT_PATH/manage.pid +daemonize=$daemonize" > $OUT_PATH/manage.ini + fi + + if [ $service = "selfpkg" -o $service = "all" ];then + query_port=$(get_config "$service" "query_port") + query_ip_addr=$(get_config "$service" "query_ip_addr") + echo "selfpkg.ini is saved to $OUT_PATH/selfpkg.ini" + echo "[uwsgi] +http=$query_ip_addr:$query_port +module=packageship.selfpkg +wsgi-file=$wsgi_file_path/selfpkg.py +callable=app +buffer-size=$buffer_size +pidfile=$OUT_PATH/selfpkg.pid +daemonize=$daemonize" > $OUT_PATH/selfpkg.ini + + fi + + rm -f config_file +} + +function start_service(){ + if [ "`ps aux | grep "uwsgi" | grep "$1.ini"`" != "" ];then + echo "!!!$1 service is running, please stop it first!!!" + else + uwsgi -d --ini $OUT_PATH/$1.ini + fi +} + +function stop_service(){ + pid=$(cat $OUT_PATH/$1.pid) + if [ "`ps aux | awk 'BEGIN{FS=" "}{if ($2=='$pid') print $0}' | grep "$1.ini"`" != "" ];then + uwsgi --$2 $OUT_PATH/$1.pid + else + echo "!!!STOP service [FAILED], Please start the service first!!!" + echo "===If the service has already exist, Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]" +} + +if [ ! -n "$1" ] +then + echo "Usages: sh packageship.sh [start|stop|restart]" + exit 0 +fi + +if [ X$2 = X ];then + service="all" +elif [ $2 = "manage" -o $2 = "selfpkg" ];then + service=$2 +else + echo "!!!can not phase the input of $2!!!" + exit 0 +fi + +create_config_file $service + +if [ $1 = start ] +then + if [ $service = "all" ];then + start_service "manage" + start_service "selfpkg" + else + start_service $service + fi + echo "===The run log is saved into $daemonize===" + +elif [ $1 = stop ];then + if [ $service = "all" ];then + stop_service "manage" "stop" + stop_service "selfpkg" "stop" + else + stop_service $service "stop" + fi + echo "===The run log is saved into $daemonize===" + +elif [ $1 = restart ];then + if [ $service = "all" ];then + stop_service "manage" "reload" + stop_service "selfpkg" "reload" + else + stop_service $service "reload" + fi + echo "===The run log is saved into $daemonize===" + +else + echo "Usages: sh packageship.sh [start|stop|restart]" +fi + diff --git a/packageship/setup.py b/packageship/setup.py index 61efa02e..09e22ca1 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -62,7 +62,6 @@ setup( long_description=open('README.md', encoding='utf-8').read(), author='gongzt', data_files=[ - (configpath, ['packageship/package.ini', - 'packageship/uWSGI_service.sh', - 'packageship/packageship.sh'])] + (configpath, ['packageship/package.ini']), + ('/usr/bin', ['packageship/pkgshipd.sh'])] ) -- Gitee From 8620c741c4d30d99d7fdc8e5bb9566f304c328b9 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 19:37:51 +0800 Subject: [PATCH 10/72] add some exception --- packageship/README.md | 12 +++++----- packageship/packageship/pkgshipd.sh | 37 +++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packageship/README.md b/packageship/README.md index 59562c27..0d457cd9 100644 --- a/packageship/README.md +++ b/packageship/README.md @@ -56,18 +56,18 @@ pkgmnt希望提供软件包依赖,生命周期,补丁查询等功能。 3.3 启动系统服务 - 单独启动manage服务: sh /etc/pkgship/uWSGI_service.sh start manage + 单独启动manage服务: sh /usr/bin/pkgshipd.sh start manage - 单独启动selfpkg服务: sh /etc/pkgship/uWSGI_service.sh start selfpkg + 单独启动selfpkg服务: sh /usr/bin/pkgshipd.sh start selfpkg - 同时启动manage和selfpkg服务: sh /etc/pkgship/uWSGI_service.sh start + 同时启动manage和selfpkg服务: sh /usr/bin/pkgshipd.sh start 3.4 停止系统服务 - 停止manage服务: sh /etc/pkgship/uWSGI_service.sh stop manage + 停止manage服务: sh /usr/bin/pkgshipd.sh stop manage - 停止selfpkg服务: sh /etc/pkgship/uWSGI_service.sh stop selfpkg + 停止selfpkg服务: sh /usr/bin/pkgshipd.sh stop selfpkg - 同时停止manage和selfpkg服务: sh /etc/pkgship/uWSGI_service.sh stop + 同时停止manage和selfpkg服务: sh /usr/bin/pkgshipd.sh stop diff --git a/packageship/packageship/pkgshipd.sh b/packageship/packageship/pkgshipd.sh index c5475ad1..f50d4ded 100644 --- a/packageship/packageship/pkgshipd.sh +++ b/packageship/packageship/pkgshipd.sh @@ -1,10 +1,15 @@ #!/bin/bash -SYS_PATH=/etc/pkgship/ +SYS_PATH=/etc/pkgship OUT_PATH=/var/run/pkgship_uwsgi if [ ! -d "$OUT_PATH" ]; then mkdir $OUT_PATH fi +if [ ! -f "$SYS_PATH/package.ini" ]; then + echo "!!!$SYS_PATH/package.ini dose not exist!!!" + exit 0 +fi + function get_config(){ cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' } @@ -17,6 +22,15 @@ function create_config_file(){ if [ $service = "manage" -o $service = "all" ];then write_port=$(get_config "$service" "write_port") write_ip_addr=$(get_config "$service" "write_ip_addr") + if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$write_ip_addr" ]] || [[ -z "$write_port" ]];then + echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" + echo "!!!The following config name is needed: daemonize, buffer_size, write_port and write_ip_addr!!!" + exit 1 + fi + if [ -z "$wsgi_file_path" ];then + echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" + exit 1 + fi echo "manage.ini is saved to $OUT_PATH/manage.ini" echo "[uwsgi] http=$write_ip_addr:$write_port @@ -31,6 +45,17 @@ daemonize=$daemonize" > $OUT_PATH/manage.ini if [ $service = "selfpkg" -o $service = "all" ];then query_port=$(get_config "$service" "query_port") query_ip_addr=$(get_config "$service" "query_ip_addr") + + if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$query_ip_addr" ]] || [[ -z "$query_port" ]];then + echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" + echo "!!!The following config name is needed: daemonize, buffer_size, query_port and query_ip_addr!!!" + exit 1 + fi + if [ -z "$wsgi_file_path" ];then + echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" + exit 1 + fi + echo "selfpkg.ini is saved to $OUT_PATH/selfpkg.ini" echo "[uwsgi] http=$query_ip_addr:$query_port @@ -60,12 +85,13 @@ function stop_service(){ uwsgi --$2 $OUT_PATH/$1.pid else echo "!!!STOP service [FAILED], Please start the service first!!!" - echo "===If the service has already exist, Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]" + echo "===If the service has already exist, Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]===" + fi } if [ ! -n "$1" ] then - echo "Usages: sh packageship.sh [start|stop|restart]" + echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" exit 0 fi @@ -79,6 +105,9 @@ else fi create_config_file $service +if [ $? -ne 0 ];then + exit 0 +fi if [ $1 = start ] then @@ -109,6 +138,6 @@ elif [ $1 = restart ];then echo "===The run log is saved into $daemonize===" else - echo "Usages: sh packageship.sh [start|stop|restart]" + echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" fi -- Gitee From d97cdfb158700c9125002c86961f04ab23a97623 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 20:18:20 +0800 Subject: [PATCH 11/72] change right for pkgshipd --- packageship/README.md | 12 +-- packageship/packageship/pkgshipd | 143 +++++++++++++++++++++++++++++++ packageship/setup.py | 2 +- 3 files changed, 150 insertions(+), 7 deletions(-) create mode 100755 packageship/packageship/pkgshipd diff --git a/packageship/README.md b/packageship/README.md index 0d457cd9..336a9466 100644 --- a/packageship/README.md +++ b/packageship/README.md @@ -56,18 +56,18 @@ pkgmnt希望提供软件包依赖,生命周期,补丁查询等功能。 3.3 启动系统服务 - 单独启动manage服务: sh /usr/bin/pkgshipd.sh start manage + 单独启动manage服务: pkgshipd start manage - 单独启动selfpkg服务: sh /usr/bin/pkgshipd.sh start selfpkg + 单独启动selfpkg服务: pkgshipd start selfpkg - 同时启动manage和selfpkg服务: sh /usr/bin/pkgshipd.sh start + 同时启动manage和selfpkg服务: pkgshipd start 3.4 停止系统服务 - 停止manage服务: sh /usr/bin/pkgshipd.sh stop manage + 停止manage服务: pkgshipd stop manage - 停止selfpkg服务: sh /usr/bin/pkgshipd.sh stop selfpkg + 停止selfpkg服务: pkgshipd stop selfpkg - 同时停止manage和selfpkg服务: sh /usr/bin/pkgshipd.sh stop + 同时停止manage和selfpkg服务: pkgshipd stop diff --git a/packageship/packageship/pkgshipd b/packageship/packageship/pkgshipd new file mode 100755 index 00000000..f50d4ded --- /dev/null +++ b/packageship/packageship/pkgshipd @@ -0,0 +1,143 @@ +#!/bin/bash +SYS_PATH=/etc/pkgship +OUT_PATH=/var/run/pkgship_uwsgi +if [ ! -d "$OUT_PATH" ]; then + mkdir $OUT_PATH +fi + +if [ ! -f "$SYS_PATH/package.ini" ]; then + echo "!!!$SYS_PATH/package.ini dose not exist!!!" + exit 0 +fi + +function get_config(){ + cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' +} + +function create_config_file(){ + echo "config type is: $service" + daemonize=$(get_config "$service" "daemonize") + buffer_size=$(get_config "$service" "buffer-size") + wsgi_file_path=$(find /usr/lib/ -name "packageship") + if [ $service = "manage" -o $service = "all" ];then + write_port=$(get_config "$service" "write_port") + write_ip_addr=$(get_config "$service" "write_ip_addr") + if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$write_ip_addr" ]] || [[ -z "$write_port" ]];then + echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" + echo "!!!The following config name is needed: daemonize, buffer_size, write_port and write_ip_addr!!!" + exit 1 + fi + if [ -z "$wsgi_file_path" ];then + echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" + exit 1 + fi + echo "manage.ini is saved to $OUT_PATH/manage.ini" + echo "[uwsgi] +http=$write_ip_addr:$write_port +module=packageship.manage +wsgi-file=$wsgi_file_path/manage.py +callable=app +buffer-size=$buffer_size +pidfile=$OUT_PATH/manage.pid +daemonize=$daemonize" > $OUT_PATH/manage.ini + fi + + if [ $service = "selfpkg" -o $service = "all" ];then + query_port=$(get_config "$service" "query_port") + query_ip_addr=$(get_config "$service" "query_ip_addr") + + if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$query_ip_addr" ]] || [[ -z "$query_port" ]];then + echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" + echo "!!!The following config name is needed: daemonize, buffer_size, query_port and query_ip_addr!!!" + exit 1 + fi + if [ -z "$wsgi_file_path" ];then + echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" + exit 1 + fi + + echo "selfpkg.ini is saved to $OUT_PATH/selfpkg.ini" + echo "[uwsgi] +http=$query_ip_addr:$query_port +module=packageship.selfpkg +wsgi-file=$wsgi_file_path/selfpkg.py +callable=app +buffer-size=$buffer_size +pidfile=$OUT_PATH/selfpkg.pid +daemonize=$daemonize" > $OUT_PATH/selfpkg.ini + + fi + + rm -f config_file +} + +function start_service(){ + if [ "`ps aux | grep "uwsgi" | grep "$1.ini"`" != "" ];then + echo "!!!$1 service is running, please stop it first!!!" + else + uwsgi -d --ini $OUT_PATH/$1.ini + fi +} + +function stop_service(){ + pid=$(cat $OUT_PATH/$1.pid) + if [ "`ps aux | awk 'BEGIN{FS=" "}{if ($2=='$pid') print $0}' | grep "$1.ini"`" != "" ];then + uwsgi --$2 $OUT_PATH/$1.pid + else + echo "!!!STOP service [FAILED], Please start the service first!!!" + echo "===If the service has already exist, Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]===" + fi +} + +if [ ! -n "$1" ] +then + echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" + exit 0 +fi + +if [ X$2 = X ];then + service="all" +elif [ $2 = "manage" -o $2 = "selfpkg" ];then + service=$2 +else + echo "!!!can not phase the input of $2!!!" + exit 0 +fi + +create_config_file $service +if [ $? -ne 0 ];then + exit 0 +fi + +if [ $1 = start ] +then + if [ $service = "all" ];then + start_service "manage" + start_service "selfpkg" + else + start_service $service + fi + echo "===The run log is saved into $daemonize===" + +elif [ $1 = stop ];then + if [ $service = "all" ];then + stop_service "manage" "stop" + stop_service "selfpkg" "stop" + else + stop_service $service "stop" + fi + echo "===The run log is saved into $daemonize===" + +elif [ $1 = restart ];then + if [ $service = "all" ];then + stop_service "manage" "reload" + stop_service "selfpkg" "reload" + else + stop_service $service "reload" + fi + echo "===The run log is saved into $daemonize===" + +else + echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" +fi + diff --git a/packageship/setup.py b/packageship/setup.py index 09e22ca1..958ce731 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -63,5 +63,5 @@ setup( author='gongzt', data_files=[ (configpath, ['packageship/package.ini']), - ('/usr/bin', ['packageship/pkgshipd.sh'])] + ('/usr/bin', ['packageship/pkgshipd'])] ) -- Gitee From 78d9bf8e39cb9c7c96730ec59dc4b544f2a03e05 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 22:28:00 +0800 Subject: [PATCH 12/72] rm pkgshipd.sh --- packageship/packageship/pkgshipd.sh | 143 ---------------------------- 1 file changed, 143 deletions(-) delete mode 100644 packageship/packageship/pkgshipd.sh diff --git a/packageship/packageship/pkgshipd.sh b/packageship/packageship/pkgshipd.sh deleted file mode 100644 index f50d4ded..00000000 --- a/packageship/packageship/pkgshipd.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash -SYS_PATH=/etc/pkgship -OUT_PATH=/var/run/pkgship_uwsgi -if [ ! -d "$OUT_PATH" ]; then - mkdir $OUT_PATH -fi - -if [ ! -f "$SYS_PATH/package.ini" ]; then - echo "!!!$SYS_PATH/package.ini dose not exist!!!" - exit 0 -fi - -function get_config(){ - cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' -} - -function create_config_file(){ - echo "config type is: $service" - daemonize=$(get_config "$service" "daemonize") - buffer_size=$(get_config "$service" "buffer-size") - wsgi_file_path=$(find /usr/lib/ -name "packageship") - if [ $service = "manage" -o $service = "all" ];then - write_port=$(get_config "$service" "write_port") - write_ip_addr=$(get_config "$service" "write_ip_addr") - if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$write_ip_addr" ]] || [[ -z "$write_port" ]];then - echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" - echo "!!!The following config name is needed: daemonize, buffer_size, write_port and write_ip_addr!!!" - exit 1 - fi - if [ -z "$wsgi_file_path" ];then - echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" - exit 1 - fi - echo "manage.ini is saved to $OUT_PATH/manage.ini" - echo "[uwsgi] -http=$write_ip_addr:$write_port -module=packageship.manage -wsgi-file=$wsgi_file_path/manage.py -callable=app -buffer-size=$buffer_size -pidfile=$OUT_PATH/manage.pid -daemonize=$daemonize" > $OUT_PATH/manage.ini - fi - - if [ $service = "selfpkg" -o $service = "all" ];then - query_port=$(get_config "$service" "query_port") - query_ip_addr=$(get_config "$service" "query_ip_addr") - - if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$query_ip_addr" ]] || [[ -z "$query_port" ]];then - echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" - echo "!!!The following config name is needed: daemonize, buffer_size, query_port and query_ip_addr!!!" - exit 1 - fi - if [ -z "$wsgi_file_path" ];then - echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" - exit 1 - fi - - echo "selfpkg.ini is saved to $OUT_PATH/selfpkg.ini" - echo "[uwsgi] -http=$query_ip_addr:$query_port -module=packageship.selfpkg -wsgi-file=$wsgi_file_path/selfpkg.py -callable=app -buffer-size=$buffer_size -pidfile=$OUT_PATH/selfpkg.pid -daemonize=$daemonize" > $OUT_PATH/selfpkg.ini - - fi - - rm -f config_file -} - -function start_service(){ - if [ "`ps aux | grep "uwsgi" | grep "$1.ini"`" != "" ];then - echo "!!!$1 service is running, please stop it first!!!" - else - uwsgi -d --ini $OUT_PATH/$1.ini - fi -} - -function stop_service(){ - pid=$(cat $OUT_PATH/$1.pid) - if [ "`ps aux | awk 'BEGIN{FS=" "}{if ($2=='$pid') print $0}' | grep "$1.ini"`" != "" ];then - uwsgi --$2 $OUT_PATH/$1.pid - else - echo "!!!STOP service [FAILED], Please start the service first!!!" - echo "===If the service has already exist, Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]===" - fi -} - -if [ ! -n "$1" ] -then - echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" - exit 0 -fi - -if [ X$2 = X ];then - service="all" -elif [ $2 = "manage" -o $2 = "selfpkg" ];then - service=$2 -else - echo "!!!can not phase the input of $2!!!" - exit 0 -fi - -create_config_file $service -if [ $? -ne 0 ];then - exit 0 -fi - -if [ $1 = start ] -then - if [ $service = "all" ];then - start_service "manage" - start_service "selfpkg" - else - start_service $service - fi - echo "===The run log is saved into $daemonize===" - -elif [ $1 = stop ];then - if [ $service = "all" ];then - stop_service "manage" "stop" - stop_service "selfpkg" "stop" - else - stop_service $service "stop" - fi - echo "===The run log is saved into $daemonize===" - -elif [ $1 = restart ];then - if [ $service = "all" ];then - stop_service "manage" "reload" - stop_service "selfpkg" "reload" - else - stop_service $service "reload" - fi - echo "===The run log is saved into $daemonize===" - -else - echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" -fi - -- Gitee From 56f607bf4044dc2b2ae06ad8d9bf0d379a639d8a Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 22 Jun 2020 22:35:47 +0800 Subject: [PATCH 13/72] rm uWSGI_service.sh --- packageship/packageship/uWSGI_service.sh | 98 ------------------------ 1 file changed, 98 deletions(-) delete mode 100644 packageship/packageship/uWSGI_service.sh diff --git a/packageship/packageship/uWSGI_service.sh b/packageship/packageship/uWSGI_service.sh deleted file mode 100644 index d5274442..00000000 --- a/packageship/packageship/uWSGI_service.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -SYS_PATH=/etc/pkgship/ -OUT_PATH=/run/pkgship_uwsgi -if [ ! -d "$OUT_PATH" ]; then - mkdir $OUT_PATH -fi - -function get_config(){ - cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' > config_file -} - -function create_config_file(){ - echo "config type is: $service" - get_config "$service" "daemonize" - daemonize=$(cat config_file) - get_config "$service" "buffer-size" - buffer_size=$(cat config_file) - wsgi_file_path=$(find /usr/lib/ -name "packageship") - if [ $service = "manage" -o $service = "all" ];then - get_config "$service" "write_port" - write_port=$(cat config_file) - get_config "$service" "write_ip_addr" - write_ip_addr=$(cat config_file) - echo "manage.ini:" - echo "[uwsgi] -http=$write_ip_addr:$write_port -module=packageship.manage -wsgi-file=$wsgi_file_path/manage.py -callable=app -buffer-size=$buffer_size -daemonize=$daemonize" > $OUT_PATH/manage.ini - cat $OUT_PATH/manage.ini - fi - - if [ $service = "selfpkg" -o $service = "all" ];then - get_config "$service" "query_port" - query_port=$(cat config_file) - get_config "$service" "query_ip_addr" - query_ip_addr=$(cat config_file) - echo "selfpkg.ini:" - echo "[uwsgi] -http=$query_ip_addr:$query_port -module=packageship.selfpkg -wsgi-file=$wsgi_file_path/selfpkg.py -callable=app -buffer-size=$buffer_size -daemonize=$daemonize" > $OUT_PATH/selfpkg.ini - cat $OUT_PATH/selfpkg.ini - - fi - - rm -f config_file -} - -if [ ! -n "$1" ] -then - echo "Usages: sh packageship.sh [start|stop|restart]" - exit 0 -fi - -if [ X$2 = X ];then - service="all" -elif [ $2 = "manage" -o $2 = "selfpkg" ];then - service=$2 -else - echo "can not phase the input of $2" - exit 0 -fi - -create_config_file $service - -if [ $1 = start ] -then - psid=`ps aux | grep "uwsgi" | grep -v "grep" | wc -l` - if [ $psid -gt 4 ] - then - echo "uwsgi is running!" - exit 0 - else - uwsgi -d --ini $OUT_PATH/manage.ini - uwsgi -d --ini $OUT_PATH/selfpkg.ini - echo "Start uwsgi service [OK], Please see the run log in $daemonize" - fi - - -elif [ $1 = stop ];then - killall -9 uwsgi - echo "Stop uwsgi service [OK], Please see the run log in $daemonize" -elif [ $1 = restart ];then - killall -9 uwsgi - uwsgi -d --ini $OUT_PATH/manage.ini - uwsgi -d --ini $OUT_PATH/selfpkg.ini - echo "Restart uwsgi service [OK], Please see the run log in $daemonize" - -else - echo "Usages: sh packageship.sh [start|stop|restart]" -fi - -- Gitee From af3cf9982530868c17cfb2a9ed08fbd55c3b94bd Mon Sep 17 00:00:00 2001 From: gongzt Date: Tue, 23 Jun 2020 09:07:58 +0800 Subject: [PATCH 14/72] =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=9A=84=E5=BC=80=E5=8F=91=E5=92=8Cpylint=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BB=A3=E7=A0=81=E6=A3=80=E6=9F=A5=E7=9A=84=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packageship/packageship/cli.py | 900 ++++++++++++++++++++++++++++ packageship/packageship/pylint.conf | 583 ++++++++++++++++++ 2 files changed, 1483 insertions(+) create mode 100644 packageship/packageship/cli.py create mode 100644 packageship/packageship/pylint.conf diff --git a/packageship/packageship/cli.py b/packageship/packageship/cli.py new file mode 100644 index 00000000..e5e8534a --- /dev/null +++ b/packageship/packageship/cli.py @@ -0,0 +1,900 @@ +''' + Entry method for custom commands +''' +import os +import json +try: + import argparse + import requests + from requests.exceptions import ConnectionError as ConnErr + from requests.exceptions import HTTPError + import prettytable + from prettytable import PrettyTable + from packageship.libs.log import Log + from packageship.libs.exception import Error + from packageship.libs.configutils.readconfig import ReadConfig + LOGGER = Log(__name__) +except ImportError as import_error: + print('Error importing related dependencies, \ + please check if related dependencies are installed') +else: + from packageship.application.apps.package.function.Constants import ResponseCode + from packageship.application.apps.package.function.Constants import ListNode + + +DB_NAME = 0 + + +def main(): + ''' + command entry function + ''' + try: + packship_cmd = PkgshipCommand() + packship_cmd.parser_args() + except Error as error: + LOGGER.logger.error(error) + print('command error') + + +class BaseCommand(): + ''' + Basic attributes used for command invocation + ''' + + def __init__(self): + self._read_config = ReadConfig() + self.write_host = None + self.read_host = None + self.__http = 'http://' + self.headers = {"Content-Type": "application/json", + "Accept-Language": "zh-CN,zh;q=0.9"} + + self.load_read_host() + self.load_write_host() + + def load_write_host(self): + ''' + Address to load write permission + ''' + wirte_port = self._read_config.get_system('write_port') + + write_ip = self._read_config.get_system('write_ip_addr') + + _write_host = self.__http + write_ip + ":" + wirte_port + + setattr(self, 'write_host', _write_host) + + def load_read_host(self): + ''' + Address to load write permission + ''' + read_port = self._read_config.get_system('query_port') + + read_ip = self._read_config.get_system('query_ip_addr') + + _read_host = self.__http + read_ip + ":" + read_port + + setattr(self, 'read_host', _read_host) + + +class PkgshipCommand(BaseCommand): + ''' + PKG package command line + ''' + parser = argparse.ArgumentParser( + description='package related dependency management') + subparsers = parser.add_subparsers( + help='package related dependency management') + + def __init__(self): + super(PkgshipCommand, self).__init__() + self.statistics = dict() + self.table = PkgshipCommand.create_table( + ['package name', 'src name', 'version', 'database']) + + # Calculate the total width of the current terminal + self.columns = int(os.popen('stty size', 'r').read().split()[1]) + self.params = [] + + @staticmethod + def register_command(command): + ''' + Register command + ''' + command.register() + + def register(self): + ''' + Command line parameter injection + ''' + for command_params in self.params: + self.parse.add_argument( # pylint: disable=E1101 + command_params[0], + type=eval(command_params[1]), # pylint: disable=W0123 + help=command_params[2], + default=command_params[3]) + + @classmethod + def parser_args(cls): + ''' + Command parsing + ''' + cls.register_command(RemoveCommand()) + cls.register_command(InitDatabaseCommand()) + cls.register_command(UpdateDatabaseCommand()) + cls.register_command(AllPackageCommand()) + cls.register_command(UpdatePackageCommand()) + cls.register_command(BuildDepCommand()) + cls.register_command(InstallDepCommand()) + cls.register_command(SelfBuildCommand()) + cls.register_command(BeDependCommand()) + cls.register_command(SingleCommand()) + try: + args = cls.parser.parse_args() + args.func(args) + except Error: + print('command error') + + def parse_package(self, response_data): + ''' + Parse the corresponding data of the package + ''' + if response_data.get('code') == ResponseCode.SUCCESS: + package_all = response_data.get('data') + if isinstance(package_all, list): + for package_item in package_all: + row_data = [package_item.get('sourceName'), package_item.get( + 'dbname'), package_item.get('version'), package_item.get('license')] + self.table.add_row(row_data) + else: + print(response_data.get('msg')) + + def parse_depend_package(self, response_data): + ''' + Parse the corresponding data of the package + ''' + bin_package_count = 0 + src_package_count = 0 + if response_data.get('code') == ResponseCode.SUCCESS: + package_all = response_data.get('data') + if isinstance(package_all, dict): + + for bin_package, package_depend in package_all.items(): + # distinguish whether the current data is the data of the root node + if isinstance(package_depend, list) and \ + package_depend[ListNode.SOURCE_NAME] != 'source': + + row_data = [bin_package, + package_depend[ListNode.SOURCE_NAME], + package_depend[ListNode.VERSION], + package_depend[ListNode.DBNAME]] + # Whether the database exists + if package_depend[ListNode.DBNAME] not in self.statistics: + self.statistics[package_depend[ListNode.DBNAME]] = { + 'binary': [], + 'source': [] + } + # Determine whether the current binary package exists + if bin_package not in \ + self.statistics[package_depend[ListNode.DBNAME]]['binary']: + self.statistics[package_depend[ListNode.DBNAME] + ]['binary'].append(bin_package) + bin_package_count += 1 + # Determine whether the source package exists + if package_depend[ListNode.SOURCE_NAME] not in \ + self.statistics[package_depend[ListNode.DBNAME]]['source']: + self.statistics[package_depend[ListNode.DBNAME]]['source'].append( + package_depend[ListNode.SOURCE_NAME]) + src_package_count += 1 + + if hasattr(self, 'table') and self.table: + self.table.add_row(row_data) + else: + LOGGER.logger.error(response_data.get('msg')) + print(response_data.get('msg')) + statistics_table = self.statistics_table( + bin_package_count, src_package_count) + return statistics_table + + def print_(self, content=None, character='=', dividing_line=False): + ''' + Output formatted characters + ''' + # Get the current width of the console + + if dividing_line: + print(character * self.columns) + if content: + print(content) + if dividing_line: + print(character * self.columns) + + @staticmethod + def create_table(title): + ''' + Create printed forms + ''' + table = PrettyTable(title) + # table.set_style(prettytable.PLAIN_COLUMNS) + table.align = 'l' + table.horizontal_char = '=' + table.junction_char = '=' + table.vrules = prettytable.NONE + table.hrules = prettytable.FRAME + return table + + def statistics_table(self, bin_package_count, src_package_count): + ''' + Generate data for total statistical tables + ''' + statistics_table = self.create_table(['', 'binary', 'source']) + statistics_table.add_row( + ['self depend sum', bin_package_count, src_package_count]) + + # cyclically count the number of source packages and binary packages in each database + for database, statistics_item in self.statistics.items(): + statistics_table.add_row([database, len(statistics_item.get( + 'binary')), len(statistics_item.get('source'))]) + return statistics_table + + @staticmethod + def http_error(response): + ''' + Log error messages for http + ''' + try: + print(response.raise_for_status()) + except HTTPError as http_error: + LOGGER.logger.error(http_error) + print('Request failed') + print(http_error) + + +class RemoveCommand(PkgshipCommand): + ''' + Delete database command + ''' + + def __init__(self): + super(RemoveCommand, self).__init__() + self.parse = PkgshipCommand.subparsers.add_parser( + 'rm', help='delete database operation') + self.params = [('db', 'str', 'name of the database operated', '')] + + def register(self): + ''' + Command line parameter injection + ''' + super(RemoveCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + if params.db is None: + print('No database specified for deletion') + else: + _url = self.write_host + '/repodatas?dbName={}'.format(params.db) + try: + response = requests.delete(_url) + except ConnErr as conn_err: + LOGGER.logger.error(conn_err) + print(str(conn_err)) + else: + # Determine whether to delete the mysql database or sqlite database + if response.status_code == 200: + data = json.loads(response.text) + if data.get('code') == ResponseCode.SUCCESS: + print('delete success') + else: + LOGGER.logger.error(data.get('msg')) + print(data.get('msg')) + else: + self.http_error(response) + + +class InitDatabaseCommand(PkgshipCommand): + ''' + Initialize database command + ''' + + def __init__(self): + super(InitDatabaseCommand, self).__init__() + self.parse = PkgshipCommand.subparsers.add_parser( + 'init', help='initialization of the database') + self.params = [ + ('-filepath', 'str', 'name of the database operated', '')] + + def register(self): + ''' + Command line parameter injection + ''' + super(InitDatabaseCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + file_path = params.filepath + try: + response = requests.post(self.write_host + + '/initsystem', data=json.dumps({'configfile': file_path}), + headers=self.headers) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + response_data = json.loads(response.text) + if response_data.get('code') == ResponseCode.SUCCESS: + print('Database initialization success ') + else: + LOGGER.logger.error(response_data.get('msg')) + print(response_data.get('msg')) + else: + self.http_error(response) + + +class UpdateDatabaseCommand(PkgshipCommand): + ''' + update database command + ''' + + def __init__(self): + super(UpdateDatabaseCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'updatedb', help='database update operation') + self.params = [('db', 'str', 'name of the database operated', '')] + + def register(self): + ''' + Command line parameter injection + ''' + super(UpdateDatabaseCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + pass # pylint: disable= W0107 + + +class AllPackageCommand(PkgshipCommand): + ''' + get all package commands + ''' + + def __init__(self): + super(AllPackageCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'list', help='get all package data') + self.table = self.create_table( + ['packagenames', 'database', 'version', 'license']) + self.params = [('-db', 'str', 'name of the database operated', '')] + + def register(self): + ''' + Command line parameter injection + ''' + super(AllPackageCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.read_host + \ + '/packages?dbName={dbName}'.format(dbName=params.db) + try: + response = requests.get(_url) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + + self.parse_package(json.loads(response.text)) + if self.table: + print(self.table) + else: + self.http_error(response) + + +class UpdatePackageCommand(PkgshipCommand): + ''' + update package data + ''' + + def __init__(self): + super(UpdatePackageCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'updatepkg', help='update package data') + self.params = [ + ('packagename', 'str', 'Source package name', ''), + ('db', 'str', 'name of the database operated', ''), + ('-m', 'str', 'Maintainers name', ''), + ('-l', 'int', 'database priority', 1) + ] + + def register(self): + ''' + Command line parameter injection + ''' + super(UpdatePackageCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.write_host + '/packages/findByPackName' + try: + response = requests.put( + _url, data=json.dumps({'sourceName': params.packagename, + 'dbName': params.db, + 'maintainer': params.m, + 'maintainLevel': params.l}), + headers=self.headers) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + data = json.loads(response.text) + if data.get('code') == ResponseCode.SUCCESS: + print('update completed') + else: + LOGGER.logger.error(data.get('msg')) + print(data.get('msg')) + else: + self.http_error(response) + + +class BuildDepCommand(PkgshipCommand): + ''' + query the compilation dependencies of the specified package + ''' + + def __init__(self): + super(BuildDepCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'builddep', help='query the compilation dependencies of the specified package') + self.collection = True + self.params = [ + ('packagename', 'str', 'source package name', ''), + ] + self.collection_params = [ + ('-dbs', 'Operational database collection') + ] + + def register(self): + ''' + Command line parameter injection + ''' + super(BuildDepCommand, self).register() + # collection parameters + + for cmd_params in self.collection_params: + self.parse.add_argument( + cmd_params[0], nargs='*', default=None, help=cmd_params[1]) + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.read_host + '/packages/findBuildDepend' + try: + response = requests.post( + _url, data=json.dumps({'sourceName': params.packagename, + 'db_list': params.dbs}), + headers=self.headers) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + statistics_table = self.parse_depend_package( + json.loads(response.text)) + if getattr(self.table, 'rowcount'): + self.print_('query {} buildDepend result display:'.format( + params.packagename)) + print(self.table) + self.print_('statistics') + print(statistics_table) + else: + self.http_error(response) + + +class InstallDepCommand(PkgshipCommand): + ''' + query the installation dependencies of the specified package + ''' + + def __init__(self): + super(InstallDepCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'installdep', help='query the installation dependencies of the specified package') + self.collection = True + self.params = [ + ('packagename', 'str', 'source package name', ''), + ] + self.collection_params = [ + ('-dbs', 'Operational database collection') + ] + + def register(self): + ''' + Command line parameter injection + ''' + super(InstallDepCommand, self).register() + # collection parameters + + for cmd_params in self.collection_params: + self.parse.add_argument( + cmd_params[0], nargs='*', default=None, help=cmd_params[1]) + self.parse.set_defaults(func=self.do_command) + + def parse_package(self, response_data): + ''' + Parse the corresponding data of the package + ''' + if getattr(self, 'statistics'): + setattr(self, 'statistics', dict()) + bin_package_count = 0 + src_package_count = 0 + if response_data.get('code') == ResponseCode.SUCCESS: + package_all = response_data.get('data') + if isinstance(package_all, dict): + for bin_package, package_depend in package_all.items(): + # distinguish whether the current data is the data of the root node + if isinstance(package_depend, list) and package_depend[-1][0][0] != 'root': + + row_data = [bin_package, + package_depend[ListNode.SOURCE_NAME], + package_depend[ListNode.VERSION], + package_depend[ListNode.DBNAME]] + # Whether the database exists + if package_depend[ListNode.DBNAME] not in self.statistics: + self.statistics[package_depend[ListNode.DBNAME]] = { + 'binary': [], + 'source': [] + } + # Determine whether the current binary package exists + if bin_package not in \ + self.statistics[package_depend[ListNode.DBNAME]]['binary']: + self.statistics[package_depend[ListNode.DBNAME] + ]['binary'].append(bin_package) + bin_package_count += 1 + # Determine whether the source package exists + if package_depend[ListNode.SOURCE_NAME] not in \ + self.statistics[package_depend[ListNode.DBNAME]]['source']: + self.statistics[package_depend[ListNode.DBNAME]]['source'].append( + package_depend[ListNode.SOURCE_NAME]) + src_package_count += 1 + + self.table.add_row(row_data) + else: + LOGGER.logger.error(response_data.get('msg')) + print(response_data.get('msg')) + # Display of aggregated data + statistics_table = self.statistics_table( + bin_package_count, src_package_count) + + return statistics_table + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.read_host + '/packages/findInstallDepend' + try: + response = requests.post(_url, data=json.dumps( + { + 'binaryName': params.packagename, + 'db_list': params.dbs + }, ensure_ascii=True), headers=self.headers) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + statistics_table = self.parse_package( + json.loads(response.text)) + if getattr(self.table, 'rowcount'): + self.print_('query{} InstallDepend result display:'.format( + params.packagename)) + print(self.table) + self.print_('statistics') + print(statistics_table) + else: + self.http_error(response) + + +class SelfBuildCommand(PkgshipCommand): + ''' + self-compiled dependency query + ''' + + def __init__(self): + super(SelfBuildCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'selfbuild', help='query the self-compiled dependencies of the specified package') + self.collection = True + self.bin_package_table = self.create_table( + ['package name', 'src name', 'version', 'database']) + self.src_package_table = self.create_table([ + 'src name', 'version', 'database']) + self.params = [ + ('packagename', 'str', 'source package name', ''), + ('-t', 'str', 'Source of data query', 'binary'), + ('-w', 'str', 'whether to include other subpackages of binary', 0), + ('-s', 'str', 'whether it is self-compiled', 0) + ] + + self.collection_params = [ + ('-dbs', 'Operational database collection') + ] + + def register(self): + ''' + Command line parameter injection + ''' + super(SelfBuildCommand, self).register() + # collection parameters + + for cmd_params in self.collection_params: + self.parse.add_argument( + cmd_params[0], nargs='*', default=None, help=cmd_params[1]) + self.parse.set_defaults(func=self.do_command) + + def _parse_bin_package(self, bin_packages): + ''' + Parsing binary result data + ''' + bin_package_count = 0 + if bin_packages: + for bin_package, package_depend in bin_packages.items(): + # distinguish whether the current data is the data of the root node + if isinstance(package_depend, list) and package_depend[-1][0][0] != 'root': + + row_data = [bin_package, package_depend[ListNode.SOURCE_NAME], + package_depend[ListNode.VERSION], package_depend[ListNode.DBNAME]] + + # Whether the database exists + if package_depend[ListNode.DBNAME] not in self.statistics: + self.statistics[package_depend[ListNode.DBNAME]] = { + 'binary': [], + 'source': [] + } + # Determine whether the current binary package exists + if bin_package not in \ + self.statistics[package_depend[ListNode.DBNAME]]['binary']: + self.statistics[package_depend[ListNode.DBNAME] + ]['binary'].append(bin_package) + bin_package_count += 1 + self.bin_package_table.add_row(row_data) + + return bin_package_count + + def _parse_src_package(self, src_apckages): + ''' + Source package data analysis + ''' + src_package_count = 0 + if src_apckages: + for src_package, package_depend in src_apckages.items(): + # distinguish whether the current data is the data of the root node + if isinstance(package_depend, list): + + row_data = [src_package, package_depend[ListNode.VERSION], + package_depend[DB_NAME]] + # Whether the database exists + if package_depend[DB_NAME] not in self.statistics: + self.statistics[package_depend[DB_NAME]] = { + 'binary': [], + 'source': [] + } + # Determine whether the current binary package exists + if src_package not in self.statistics[package_depend[DB_NAME]]['source']: + self.statistics[package_depend[DB_NAME] + ]['source'].append(src_package) + src_package_count += 1 + + self.src_package_table.add_row(row_data) + + return src_package_count + + def parse_package(self, response_data): + ''' + Parse the corresponding data of the package + ''' + if getattr(self, 'statistics'): + setattr(self, 'statistics', dict()) + bin_package_count = 0 + src_package_count = 0 + + if response_data.get('code') == ResponseCode.SUCCESS: + package_all = response_data.get('data') + if isinstance(package_all, dict): + # Parsing binary result data + bin_package_count = self._parse_bin_package( + package_all.get('binary_dicts')) + + # Source package data analysis + src_package_count = self._parse_src_package( + package_all.get('source_dicts')) + else: + LOGGER.logger.error(response_data.get('msg')) + print(response_data.get('msg')) + # Display of aggregated data + statistics_table = self.statistics_table( + bin_package_count, src_package_count) + # return (bin_package_table, src_package_table, statistics_table) + return statistics_table + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.read_host + '/packages/findSelfDepend' + try: + response = requests.post(_url, + data=json.dumps({ + 'packagename': params.packagename, + 'db_list': params.dbs, + 'packtype': params.t, + 'selfbuild': str(params.s), + 'withsubpack': str(params.w)}), + headers=self.headers) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + statistics_table = self.parse_package( + json.loads(response.text)) + if getattr(self.bin_package_table, 'rowcount') \ + and getattr(self.src_package_table, 'rowcount'): + self.print_('query {} selfDepend result display :'.format( + params.packagename)) + print(self.bin_package_table) + self.print_(character='=') + print(self.src_package_table) + self.print_('statistics') + print(statistics_table) + else: + self.http_error(response) + + +class BeDependCommand(PkgshipCommand): + ''' + dependent query + ''' + + def __init__(self): + super(BeDependCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'bedepend', help='dependency query for the specified package') + self.params = [ + ('packagename', 'str', 'source package name', ''), + ('db', 'str', 'name of the database operated', ''), + ('-w', 'str', 'whether to include other subpackages of binary', 0), + ] + + def register(self): + ''' + Command line parameter injection + ''' + super(BeDependCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.read_host + '/packages/findBeDepend' + try: + response = requests.post(_url, data=json.dumps( + { + 'packagename': params.packagename, + 'dbname': params.db, + 'withsubpack': str(params.w) + } + ), headers=self.headers) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + statistics_table = self.parse_depend_package( + json.loads(response.text)) + if getattr(self.table, 'rowcount'): + self.print_('query {} beDepend result display :'.format( + params.packagename)) + print(self.table) + self.print_('statistics') + print(statistics_table) + else: + self.http_error(response) + + +class SingleCommand(PkgshipCommand): + ''' + query single package information + ''' + + def __init__(self): + super(SingleCommand, self).__init__() + + self.parse = PkgshipCommand.subparsers.add_parser( + 'single', help='query the information of a single package') + self.params = [ + ('packagename', 'str', 'source package name', ''), + ('-db', 'str', 'name of the database operated', '') + ] + + def register(self): + ''' + Command line parameter injection + ''' + super(SingleCommand, self).register() + self.parse.set_defaults(func=self.do_command) + + def parse_package(self, response_data): + ''' + Parse the corresponding data of the package + ''' + show_field_name = ('sourceName', 'dbname', 'version', + 'license', 'maintainer', 'maintainlevel') + print_contents = [] + if response_data.get('code') == ResponseCode.SUCCESS: + package_all = response_data.get('data') + if isinstance(package_all, list): + for package_item in package_all: + for key, value in package_item.items(): + if value is None: + value = '' + if key in show_field_name: + + line_content = '%-15s:%s' % (key, value) + print_contents.append(line_content) + print_contents.append('='*self.columns) + else: + print(response_data.get('msg')) + if print_contents: + for content in print_contents: + self.print_(content=content) + + def do_command(self, params): + ''' + Action to execute command + ''' + _url = self.read_host + \ + '/packages/findByPackName?dbName={db_name}&sourceName={packagename}' \ + .format(db_name=params.db, packagename=params.packagename) + try: + response = requests.get(_url) + except ConnectionError as conn_error: + LOGGER.logger.error(conn_error) + print(str(conn_error)) + else: + if response.status_code == 200: + self.parse_package(json.loads(response.text)) + else: + self.http_error(response) + + +if __name__ == '__main__': + main() diff --git a/packageship/packageship/pylint.conf b/packageship/packageship/pylint.conf new file mode 100644 index 00000000..c2e270f5 --- /dev/null +++ b/packageship/packageship/pylint.conf @@ -0,0 +1,583 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + attribute-defined-outside-init + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + _rows + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=6 + +# Maximum number of attributes for a class (see R0902). +max-attributes=15 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception -- Gitee From cdc8a98e1ae5ea7202869a75aca4c0472cd959dd Mon Sep 17 00:00:00 2001 From: gongzt Date: Tue, 23 Jun 2020 10:04:46 +0800 Subject: [PATCH 15/72] Database initialization function, optimization of public libraries --- .../application/initsystem/__init__.py | 0 .../application/initsystem/data_import.py | 759 ++++++++++++++++++ .../application/initsystem/datamerge.py | 283 +++++++ .../packageship/application/models/package.py | 101 +++ .../application/models/temporarydb.py | 85 ++ packageship/packageship/libs/__init__.py | 0 .../libs/configutils/readconfig.py | 4 +- .../libs/dbutils/sqlalchemy_helper.py | 39 +- packageship/packageship/libs/exception/ext.py | 8 +- packageship/packageship/libs/log/loghelper.py | 24 +- 10 files changed, 1279 insertions(+), 24 deletions(-) create mode 100644 packageship/packageship/application/initsystem/__init__.py create mode 100644 packageship/packageship/application/initsystem/data_import.py create mode 100644 packageship/packageship/application/initsystem/datamerge.py create mode 100644 packageship/packageship/application/models/package.py create mode 100644 packageship/packageship/application/models/temporarydb.py create mode 100644 packageship/packageship/libs/__init__.py diff --git a/packageship/packageship/application/initsystem/__init__.py b/packageship/packageship/application/initsystem/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py new file mode 100644 index 00000000..e264ecb6 --- /dev/null +++ b/packageship/packageship/application/initsystem/data_import.py @@ -0,0 +1,759 @@ +''' + Initialization of data import +    Import the data in the sqlite database into the mysql database +''' +import os +import pathlib +import yaml +from sqlalchemy.exc import SQLAlchemyError, InternalError +from packageship.libs.dbutils.sqlalchemy_helper import DBHelper +from packageship.libs.exception import ContentNoneException +from packageship.libs.exception import DatabaseRepeatException +from packageship.libs.exception import DataMergeException +from packageship.libs.exception import Error +from packageship.libs.configutils.readconfig import ReadConfig +from packageship.libs.log import Log +from packageship.application.models.package import bin_pack, src_pack, pack_requires, pack_provides +from packageship.application.initsystem.datamerge import MergeData +from packageship.application.models.temporarydb import src_package +from packageship.application.models.temporarydb import src_requires +from packageship.application.models.temporarydb import bin_package +from packageship.application.models.temporarydb import bin_requiresment +from packageship.application.models.temporarydb import bin_provides +from packageship.system_config import DATABASE_SUCCESS_FILE +from packageship.system_config import DATABASE_FOLDER_PATH + +LOGGER = Log(__name__) + + +class InitDataBase(): + ''' + Database initialization, generate multiple databases and data based on configuration files + ''' + + def __init__(self, config_file_path=None): + ''' + parameter: + config_file_path:The path of the configuration file that needs to initialize + the database in the project + ''' + + self.config_file_path = config_file_path + + if self.config_file_path: + # yaml configuration file content + self.config_file_datas = self.__read_config_file() + + self._read_config = ReadConfig() + + self.db_type = self._read_config.get_database('dbtype') + + if self.db_type is None: + self.db_type = 'mysql' + + if self.db_type not in ['mysql', 'sqlite']: + LOGGER.logger.error("database type configuration error") + raise Exception('database type configuration error') + + def __read_config_file(self): + ''' + functional description: + Read the contents of the configuration file load each node data in the yaml + configuration file as a list to return + ''' + + if not os.path.exists(self.config_file_path): + raise FileNotFoundError( + 'system initialization configuration file does not exist') + # load yaml configuration file + with open(self.config_file_path, 'r', encoding='utf-8') as file_context: + init_database_config = yaml.load( + file_context.read(), Loader=yaml.FullLoader) + if init_database_config is None: + raise ContentNoneException( + 'The content of the database initialization configuration file cannot be empty') + if not isinstance(init_database_config, list): + raise TypeError('wrong type of configuration file') + + return init_database_config + + def init_data(self): + ''' + functional description:Initialization of the database + ''' + if getattr(self, 'config_file_datas', None) is None or \ + self.config_file_datas is None: + raise ContentNoneException('Initialization file content is empty') + + if self.__exists_repeat_database(): + raise DatabaseRepeatException( + 'There is a duplicate database configuration') + if not InitDataBase.delete_settings_file(): + raise IOError( + 'An error occurred while deleting the database configuration file') + + # Create a database maintained by benchmark information + if self.db_type == 'mysql': + MysqlDatabaseOperations( + 'maintenance.information').create_datum_database() + else: + SqliteDatabaseOperations( + 'maintenance.information').create_datum_database() + + for database in self.config_file_datas: + if not isinstance(database, dict): + continue + if not database.get('dbname'): + continue + priority = database.get('priority') + if not isinstance(priority, int) or priority < 0 or priority > 100: + continue + status = database.get('status') + + # Initialization data + self._init_data(database) + + def _create_database(self, database): + ''' + create related databases + ''' + db_name = database.get('dbname') + self._sqlite_db = SqliteDatabaseOperations(db_name=db_name) + + if self.db_type == 'mysql': + creatadatabase = MysqlDatabaseOperations(db_name) + if not creatadatabase.create_database(): + raise SQLAlchemyError("failed to create database or table") + return db_name + + sqltedb_file = self._sqlite_db.create_sqlite_database() + if sqltedb_file is None: + raise SQLAlchemyError( + "failed to create database or table") + return sqltedb_file + + def _init_data(self, database): + ''' + functional description:data initialization operation + parameter: + database:database configuration information + ''' + try: + # 1. create a database and related tables in the database + db_name = self._create_database(database) + # 2. get the data of binary packages and source packages + src_package_paths = database.get('src_db_file') + bin_package_paths = database.get('bin_db_file') + + if src_package_paths is None or bin_package_paths is None: + raise ContentNoneException( + 'The configured database file path is empty') + if not isinstance(src_package_paths, list) \ + or not isinstance(bin_package_paths, list): + raise TypeError( + 'The source code and binary path types in the initialization file are abnormal') + # 3. Obtain temporary source package files and binary package files + db_file = self.file_merge( + src_package_paths, bin_package_paths) + # 4. dependencies between combined data + self.data_relationship(db_file) + # # 5. save data + self.save_data(db_name) + + except (SQLAlchemyError, ContentNoneException, + DataMergeException, TypeError, Error) as error_msg: + # Delete the specified database + try: + if self.db_type == 'mysql': + MysqlDatabaseOperations.drop_database( + database.get('dbname')) + else: + self._sqlite_db.drop_database() + + except (IOError, Error) as exception_msg: + LOGGER.logger.error(exception_msg) + else: + # Update the configuration file of the database + database_content = { + 'database_name': database.get('dbname'), + 'priority': database.get('priority'), + 'status': database.get('status') + } + InitDataBase.__updata_settings_file( + database_content=database_content) + finally: + try: + if os.path.exists(db_file): + os.remove(db_file) + except (IOError, UnboundLocalError) as error_msg: + LOGGER.logger.error(error_msg) + + def _src_package_relation(self, src_package_data): + ''' + Mapping of data relations of source packages + ''' + _src_package_name = src_package_data.name + _src_package = { + "name": src_package_data.name, + "version": src_package_data.version, + "license": src_package_data.rpm_license, + "sourceURL": src_package_data.url, + "Maintaniner": src_package_data.maintaniner + } + if _src_package_name not in self._src_packages.keys(): + self._src_packages[_src_package_name] = _src_package + else: + # Determine the version number + if src_package_data.version > \ + self._src_packages[_src_package_name]['version']: + + self._src_packages[_src_package_name] = _src_package + # Delete previous version + for key in [names[0] for names in self._src_package_names.items() + if _src_package_name == names[1]]: + del self._src_package_names[key] + + self._src_package_names[src_package_data.pkgKey] = _src_package_name + + def _src_requires_relation(self, src_requires_data): + ''' + Source package dependent package data relationship mapping + ''' + _src_package_name = self._src_package_names.get( + src_requires_data.pkgKey) + if _src_package_name: + if _src_package_name not in self._src_requires_dicts.keys(): + self._src_requires_dicts[_src_package_name] = [] + self._src_requires_dicts[_src_package_name].append({ + 'name': src_requires_data.name + }) + + def _bin_package_relation(self, bin_package_data): + ''' + Binary package relationship mapping problem + ''' + _bin_pkg_key = bin_package_data.pkgKey + self._bin_package_name[bin_package_data.name] = _bin_pkg_key + + src_package_name = bin_package_data.src_pack_name + _bin_package = { + 'name': bin_package_data.name, + 'version': bin_package_data.version, + 'relation': True + } + if src_package_name not in self._bin_package_dicts.keys(): + self._bin_package_dicts[src_package_name] = [] + + # Determine whether the version number is consistent with the source code package + # If they are the same, an association relationship is established. + for index, bin_package_item in enumerate(self._bin_package_dicts[src_package_name]): + if bin_package_item.get('name') == bin_package_data.name: + if bin_package_item.get('version') < bin_package_data.version: + self._bin_package_dicts[src_package_name][index]['relation'] = False + + self._bin_package_dicts[src_package_name].append(_bin_package) + # self._bin_package_dicts[src_package_name].append(_bin_package) + # self._bin_package_dicts[src_package_name].sort( + # key=lambda x: x.get('version')) + # self._bin_package_dicts[src_package_name][-1]['relation'] = True + # for bin_package_item in self._bin_package_dicts[src_package_name][:-1]: + # bin_package_item['relation'] = False + + def _bin_requires_relation(self, bin_requires_data): + ''' + Binary package dependency package relationship mapping problem + ''' + _bin_pkg_key = bin_requires_data.pkgKey + if _bin_pkg_key: + if _bin_pkg_key not in self._bin_requires_dicts: + self._bin_requires_dicts[_bin_pkg_key] = [] + + self._bin_requires_dicts[_bin_pkg_key].append({ + 'name': bin_requires_data.name + }) + + def _bin_provides_relation(self, bin_provides_data): + ''' + Binary package provided by the relationship mapping problem + ''' + _bin_pkg_key = bin_provides_data.pkgKey + if _bin_pkg_key: + if _bin_pkg_key not in self._bin_provides_dicts: + self._bin_provides_dicts[_bin_pkg_key] = [] + self._bin_provides_dicts[_bin_pkg_key].append({ + 'name': bin_provides_data.name + }) + + def data_relationship(self, db_file): + ''' + dependencies between combined data + ''' + self._bin_provides_dicts = dict() + self._bin_requires_dicts = dict() + self._bin_package_name = dict() + self._bin_package_dicts = dict() + self._src_requires_dicts = dict() + self._src_packages = dict() + self._src_package_names = dict() + try: + with DBHelper(db_name=db_file, db_type='sqlite:///') as database: + # source package data + for src_package_item in database.session.query(src_package).all(): + self._src_package_relation(src_package_item) + + # combine all dependent packages of source packages + for src_requires_item in database.session.query(src_requires).all(): + self._src_requires_relation(src_requires_item) + + # combine all binary packages + for bin_package_item in database.session.query(bin_package).all(): + self._bin_package_relation(bin_package_item) + + # combine all dependent packages under the current binary package + for bin_requires_item in database.session.query( + bin_requiresment).all(): + self._bin_requires_relation(bin_requires_item) + + # combine the packages provided by the current binary package + + for bin_provides_item in database.session.query(bin_provides).all(): + self._bin_provides_relation(bin_provides_item) + + except Error as error_msg: + LOGGER.logger.error(error_msg) + + def file_merge(self, src_package_paths, bin_package_paths): + ''' + integration of multiple data files + ''' + _db_file = os.path.join( + self._sqlite_db.database_file_folder, 'temporary_database') + + if os.path.exists(_db_file): + os.remove(_db_file) + # create a temporary sqlite database + with DBHelper(db_name=_db_file, db_type='sqlite:///') as database: + tables = ['src_package', 'src_requires', + 'bin_package', 'bin_requiresment', 'bin_provides'] + database.create_table(tables) + + _src_package_key = 1 + # load all source package files and import the files into it + for src_file in src_package_paths: + load_sqlite_data = MergeData(db_file=src_file) + + # Combine data from all source packages + + _src_package_key, src_merge_result = load_sqlite_data.src_file_merge( + src_package_key=_src_package_key, db_file=_db_file) + if not src_merge_result: + raise DataMergeException( + 'abnormal multi-file database integration') + + # load binary package file + _bin_package_key = 1 + for bin_file in bin_package_paths: + load_sqlite_data = MergeData(db_file=bin_file) + + # Combine all binary package data + _bin_package_key, bin_merge_result = load_sqlite_data.bin_file_merge( + bin_package_key=_bin_package_key, db_file=_db_file) + if not bin_merge_result: + raise DataMergeException( + 'abnormal multi-file database integration') + return _db_file + + def __exists_repeat_database(self): + ''' + functional description:Determine if the same database name exists + parameter: + return value: + exception description: + modify record: + ''' + + db_names = [name.get('dbname') + for name in self.config_file_datas] + + if len(set(db_names)) != len(self.config_file_datas): + return True + + return False + + def _save_bin_package(self, src_packages): + ''' + Save binary package data + ''' + bin_packages = [] + for package_data in src_packages: + try: + bin_package_datas = self._bin_package_dicts.pop( + package_data.name) + except KeyError: + bin_package_datas = None + + if bin_package_datas: + for bin_package_item in bin_package_datas: + bin_package_dict = { + 'name': bin_package_item.get('name'), + 'version': bin_package_item.get('version'), + 'srcIDkey': None + } + if bin_package_item.get('relation'): + bin_package_dict['srcIDkey'] = package_data.id + bin_packages.append(bin_package_dict) + + # source package dependency package + src_requires_datas = self._src_requires_dicts.get( + package_data.name) + if src_requires_datas: + for src_requires_item in src_requires_datas: + requires_name = src_requires_item.get('name') + if requires_name: + if requires_name not in self.requires.keys(): + self.requires[requires_name] = [] + self.requires[requires_name].append({ + 'name': src_requires_item.get('name'), + 'srcIDkey': package_data.id, + 'depProIDkey': None, + 'binIDkey': None + }) + + # organization independent binary package + + for bin_packs in self._bin_package_dicts.values(): + for bin_pack_item in bin_packs: + bin_packages.append({ + 'name': bin_pack_item.get('name'), + 'version': bin_pack_item.get('version'), + 'srcIDkey': None + }) + return bin_packages + + def _save_bin_provides(self, bin_packages): + ''' + Save package data provided by binary + ''' + bin_provides_list = [] + for bin_pack_entity in bin_packages: + + # Get the pkgKey of the current binary package + pkg_key = self._bin_package_name.get(bin_pack_entity.name) + + if self._bin_requires_dicts.get(pkg_key): + for bin_requires_item in self._bin_requires_dicts.get(pkg_key): + requires_name = bin_requires_item.get('name') + if requires_name: + if requires_name not in self.requires.keys(): + self.requires[requires_name] = [] + self.requires[requires_name].append({ + 'name': bin_requires_item.get('name'), + 'binIDkey': bin_pack_entity.id, + 'depProIDkey': None, + 'srcIDkey': None + }) + + if self._bin_provides_dicts.get(pkg_key): + for bin_provides_item in self._bin_provides_dicts.get(pkg_key): + bin_provides_list.append({ + 'name': bin_provides_item.get('name'), + 'binIDkey': bin_pack_entity.id + }) + return bin_provides_list + + def save_data(self, db_name): + ''' + save related package data + ''' + with DBHelper(db_name=db_name) as data_base: + # Add source package data + data_base.batch_add( + [src_package_item[1] for src_package_item in self._src_packages.items()], src_pack) + + self.requires = dict() + + # Save dependency data of binary packages and source packages + + data_base.batch_add(self._save_bin_package( + data_base.session.query(src_pack).all()), bin_pack) + + # Save all packages and dependent packages provided by the binary package + + data_base.batch_add(self._save_bin_provides( + data_base.session.query(bin_pack).all()), pack_provides) + + all_requires = [] + for provides_item in data_base.session.query(pack_provides).all(): + if provides_item.name in self.requires.keys(): + for requires_item in self.requires[provides_item.name]: + requires_item['depProIDkey'] = provides_item.id + all_requires.append(requires_item) + + data_base.batch_add(all_requires, pack_requires) + + @staticmethod + def __updata_settings_file(**Kwargs): + ''' + update some configuration files related to the database in the system + parameter: + **Kwargs:data related to configuration file nodes + database_name: Name database + priority: priority + ''' + try: + if not os.path.exists(DATABASE_SUCCESS_FILE): + pathlib.Path(DATABASE_SUCCESS_FILE).touch() + with open(DATABASE_SUCCESS_FILE, 'a+', encoding='utf8') as file_context: + setting_content = [] + if 'database_content' in Kwargs.keys(): + content = Kwargs.get('database_content') + if content: + setting_content.append(content) + yaml.dump(setting_content, file_context) + + except FileNotFoundError as not_found: + LOGGER.logger.error(not_found) + except IOError as exception_msg: + LOGGER.logger.error(exception_msg) + + @staticmethod + def delete_settings_file(): + ''' + functional description:Delete the configuration file of the database + parameter: + return value: + exception description: + modify record: + ''' + try: + if os.path.exists(DATABASE_SUCCESS_FILE): + os.remove(DATABASE_SUCCESS_FILE) + except (IOError, Error) as exception_msg: + LOGGER.logger.error(exception_msg) + return False + else: + return True + + def delete_db(self, db_name): + ''' + Delete the database + ''' + if self.db_type == 'mysql': + del_result = MysqlDatabaseOperations.drop_database(db_name) + else: + if not hasattr(self, '_sqlite_db'): + self._sqlite_db = SqliteDatabaseOperations(db_name=db_name) + del_result = self._sqlite_db.drop_database() + + if del_result: + try: + file_read = open(DATABASE_SUCCESS_FILE, 'r', encoding='utf-8') + _databases = yaml.load( + file_read.read(), Loader=yaml.FullLoader) + for database in _databases: + if database.get('database_name') == db_name: + _databases.remove(database) + # Delete the successfully imported database configuration node + with open(DATABASE_SUCCESS_FILE, 'w+', encoding='utf-8') as file_context: + yaml.safe_dump(_databases, file_context) + except (IOError, Error) as del_config_error: + LOGGER.logger.error(del_config_error) + finally: + file_read.close() + + +class MysqlDatabaseOperations(): + ''' + Related to database operations, creating databases, creating tables + ''' + + def __init__(self, db_name): + + self.db_name = db_name + self.create_database_sql = ''' CREATE DATABASE if not exists `{db_name}` \ + DEFAULT CHARACTER SET utf8mb4; '''.format(db_name=self.db_name) + self.drop_database_sql = '''drop DATABASE if exists `{db_name}` '''.format( + db_name=self.db_name) + + def create_database(self): + ''' + functional description:create a database + ''' + + with DBHelper(db_name='mysql') as data_base: + + try: + # create database + data_base.session.execute(self.drop_database_sql) + data_base.session.execute(self.create_database_sql) + except SQLAlchemyError as exception_msg: + LOGGER.logger.error(exception_msg) + return False + else: + # create tables + return self.__create_tables() + + @classmethod + def drop_database(cls, db_name): + ''' + functional description:Delete the database according to the specified name + parameter: + :db_name:The name of the database + return value: + exception description: + modify record: + ''' + if db_name is None: + raise IOError( + "The name of the database to be deleted cannot be empty") + with DBHelper(db_name='mysql') as data_base: + drop_database = ''' drop DATABASE if exists `{db_name}` '''.format( + db_name=db_name) + try: + data_base.session.execute(drop_database) + except SQLAlchemyError as exception_msg: + LOGGER.logger.error(exception_msg) + return False + else: + return True + + def __create_tables(self): + ''' + Create the specified data table + ''' + try: + with DBHelper(db_name=self.db_name) as database: + tables = ['src_pack', 'bin_pack', 'pack_provides', + 'pack_requires'] + database.create_table(tables) + + except SQLAlchemyError as exception_msg: + LOGGER.logger.error(exception_msg) + return False + else: + return True + + def create_datum_database(self): + ''' + Create a benchmark database to save the maintainer's information + ''' + with DBHelper(db_name='mysql') as data_base: + # create database + try: + data_base.session.execute(self.create_database_sql) + except SQLAlchemyError as exception_msg: + LOGGER.logger.error(exception_msg) + return False + else: + # create tables + return self.__create_datum_tables() + + def __create_datum_tables(self): + ''' + Create a data table of maintainer information + ''' + try: + with DBHelper(db_name=self.db_name) as database: + tables = ['maintenance_info'] + database.create_table(tables) + except InternalError as exists_table_err: + LOGGER.logger.error(exists_table_err) + return True + except (SQLAlchemyError, Error) as exception_msg: + LOGGER.logger.error(exception_msg) + return False + + else: + return True + + +class SqliteDatabaseOperations(): + ''' + sqlite database related operations + ''' + + def __init__(self, db_name, **kwargs): + + self.db_name = db_name + self._read_config = ReadConfig() + if getattr(kwargs, 'database_path', None) is None: + self.database_file_path() + else: + self.database_file_folder = kwargs.get('database_path') + + def database_file_path(self): + ''' + Database file path + ''' + self.database_file_folder = self._read_config.get_system( + 'data_base_path') + if not self.database_file_folder: + self.database_file_folder = DATABASE_FOLDER_PATH + + if not os.path.exists(self.database_file_folder): + try: + os.makedirs(self.database_file_folder) + except IOError as makedirs_error: + LOGGER.logger.error(makedirs_error) + self.database_file_folder = None + + def create_sqlite_database(self): + ''' + create sqlite database and table + ''' + if self.database_file_folder is None: + raise FileNotFoundError('Database folder does not exist') + + _db_file = os.path.join( + self.database_file_folder, self.db_name) + + if os.path.exists(_db_file+'.db'): + os.remove(_db_file + '.db') + + # create a sqlite database + with DBHelper(db_name=_db_file) as database: + tables = ['src_pack', 'bin_pack', + 'pack_requires', 'pack_provides'] + try: + database.create_table(tables) + except SQLAlchemyError as create_table_err: + LOGGER.logger.error(create_table_err) + return None + + return _db_file + + def drop_database(self): + ''' + Delete the specified sqlite database + ''' + try: + db_path = os.path.join( + self.database_file_folder, self.db_name+'.db') + if os.path.exists(db_path): + os.remove(db_path) + except IOError as exception_msg: + LOGGER.logger.error(exception_msg) + return False + else: + return True + + def create_datum_database(self): + ''' + create sqlite database and table + ''' + if self.database_file_folder is None: + raise FileNotFoundError('Database folder does not exist') + + _db_file = os.path.join( + self.database_file_folder, self.db_name) + + if not os.path.exists(_db_file+'.db'): + # create a sqlite database + with DBHelper(db_name=_db_file) as database: + tables = ['maintenance_info'] + try: + database.create_table(tables) + except SQLAlchemyError as create_table_err: + LOGGER.logger.error(create_table_err) + return None + return _db_file diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py new file mode 100644 index 00000000..cea7512a --- /dev/null +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -0,0 +1,283 @@ +''' + Integration of multiple sqlite file data, including + reading sqlite database and inserting data +''' +from sqlalchemy.exc import SQLAlchemyError +from packageship.application.models.temporarydb import src_package +from packageship.application.models.temporarydb import src_requires +from packageship.application.models.temporarydb import bin_package +from packageship.application.models.temporarydb import bin_requiresment +from packageship.application.models.temporarydb import bin_provides +from packageship.application.models.package import maintenance_info +from packageship.libs.dbutils import DBHelper +from packageship.libs.log import Log + +LOGGER = Log(__name__) + + +class MergeData(): + ''' + Load data from sqlite database + ''' + + def __init__(self, db_file): + + self.db_file = db_file + self.db_type = 'sqlite:///' + self.datum_database = 'maintenance.information' + + @staticmethod + def __columns(cursor): + ''' + functional description:Returns all the column names queried by the current cursor + ''' + + return [col[0] for col in cursor.description] + + def get_package_data(self): + ''' + get binary package or source package data + ''' + try: + with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ + as database: + src_packages_data = database.session.execute( + "select pkgKey,name,version,rpm_license,url,rpm_sourcerpm from packages") + columns = MergeData.__columns( + src_packages_data.cursor) + return [dict(zip(columns, row)) for row in src_packages_data.fetchall()] + except SQLAlchemyError as sql_error: + LOGGER.logger.error(sql_error) + return None + + def get_requires_data(self): + ''' + get dependent package data of binary package or source package + ''' + try: + with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ + as database: + requires = database.session.execute( + "select pkgKey,name from requires") + columns = MergeData.__columns(requires.cursor) + return [dict(zip(columns, row)) for row in requires.fetchall()] + except SQLAlchemyError as sql_error: + LOGGER.logger.error(sql_error) + return None + + def get_provides(self): + ''' + get the dependency package provided by the binary package + ''' + try: + with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ + as database: + requires = database.session.execute( + "select pkgKey,name from provides") + columns = MergeData.__columns(requires.cursor) + return [dict(zip(columns, row)) for row in requires.fetchall()] + except SQLAlchemyError as sql_error: + LOGGER.logger.error(sql_error) + return None + + def get_maintenance_info(self): + ''' + Obtain the information of the maintainer + ''' + try: + if not hasattr(self, 'mainter_infos'): + self.mainter_infos = dict() + with DBHelper(db_name=self.datum_database) as database: + for info in database.session.query(maintenance_info).all(): + if info.name not in self.mainter_infos.keys(): + self.mainter_infos[info.name] = [] + self.mainter_infos[info.name].append({ + 'version': info.version, + 'maintaniner': info.maintaniner + }) + except SQLAlchemyError as sql_error: + LOGGER.logger.error(sql_error) + + def src_file_merge(self, src_package_key, db_file): + ''' + Source code related data integration + ''' + self.get_maintenance_info() + + self.__compose_src_package() + + self.__compose_src_rquires() + + # Combination of relationships between source packages and dependent packages + src_requires_data = [] + for src_package_item in self.src_package_datas: + src_package_key += 1 + requires = self.src_requires_dicts.get( + src_package_item.get('pkgKey')) + if requires: + for src_requires_item in requires: + src_requires_item['pkgKey'] = src_package_key + src_requires_data.append(src_requires_item) + src_package_item['pkgKey'] = src_package_key + + try: + with DBHelper(db_name=db_file, db_type=self.db_type) as data_base: + data_base.batch_add(self.src_package_datas, src_package) + data_base.batch_add(src_requires_data, src_requires) + except SQLAlchemyError as sql_error: + LOGGER.logger.error(sql_error) + return (0, False) + else: + return (src_package_key, True) + + def __compose_src_package(self): + ''' + Combine source package data + ''' + if getattr(self, 'src_package_datas', None) is None: + self.src_package_datas = [] + + for src_package_item in self.get_package_data(): + src_package_name = src_package_item.get('name') + if src_package_name: + # Find the maintainer information of the current data + maintenance_infos = self.mainter_infos.get(src_package_name) + maintenance = None + if self.mainter_infos.get(src_package_name): + maintenance = list(filter(lambda x: x.get( + 'version') == src_package_item.get('version'), maintenance_infos)) + self.src_package_datas.append( + { + "name": src_package_item.get('name'), + "version": src_package_item.get('version'), + "rpm_license": src_package_item.get('rpm_license'), + "url": src_package_item.get('url'), + "pkgKey": src_package_item.get('pkgKey'), + 'maintaniner': + maintenance[0].get('maintaniner') if maintenance and len( + maintenance) > 0 else None + } + ) + + def __compose_src_rquires(self): + ''' + Combine source package dependent package data + ''' + if getattr(self, 'src_requires_dicts', None) is None: + self.src_requires_dicts = dict() + + for src_requires_item in self.get_requires_data(): + pkg_key = src_requires_item.get('pkgKey') + if pkg_key: + if pkg_key not in self.src_requires_dicts.keys(): + self.src_requires_dicts[pkg_key] = [] + self.src_requires_dicts[pkg_key].append( + { + 'name': src_requires_item.get('name'), + 'pkgKey': pkg_key + } + ) + + def __compose_bin_package(self): + ''' + Combine binary package data + ''' + if getattr(self, 'bin_package_datas', None) is None: + self.bin_package_datas = [] + + for bin_package_item in self.get_package_data(): + try: + src_package_name = bin_package_item.get('rpm_sourcerpm').split( + '-' + bin_package_item.get('version'))[0] + except AttributeError as exception_msg: + src_package_name = None + LOGGER.logger.warning(exception_msg) + else: + self.bin_package_datas.append( + { + "name": bin_package_item.get('name'), + "version": bin_package_item.get('version'), + "license": bin_package_item.get('rpm_license'), + "sourceURL": bin_package_item.get('url'), + "src_pack_name": src_package_name, + "pkgKey": bin_package_item.get('pkgKey') + } + ) + + def __compose_bin_requires(self): + ''' + Combining binary dependent package data + ''' + if getattr(self, 'bin_requires_dicts', None) is None: + self.bin_requires_dicts = dict() + + for bin_requires_item in self.get_requires_data(): + pkg_key = bin_requires_item.get('pkgKey') + if pkg_key: + if pkg_key not in self.bin_requires_dicts.keys(): + self.bin_requires_dicts[pkg_key] = [] + self.bin_requires_dicts[pkg_key].append({ + 'name': bin_requires_item.get('name'), + 'pkgKey': 0 + }) + + def __compose_bin_provides(self): + ''' + Combine binary package data + ''' + if getattr(self, 'bin_provides_dicts', None) is None: + self.bin_provides_dicts = dict() + + for bin_provides_item in self.get_provides(): + pkg_key = bin_provides_item.get('pkgKey') + if pkg_key: + if pkg_key not in self.bin_provides_dicts.keys(): + self.bin_provides_dicts[pkg_key] = [] + self.bin_provides_dicts[pkg_key].append({ + 'name': bin_provides_item.get('name'), + 'pkgKey': 0 + }) + + def bin_file_merge(self, bin_package_key, db_file): + ''' + Binary package related data integration + ''' + + self.__compose_bin_package() + # binary package dependent package integration + + self.__compose_bin_requires() + + self.__compose_bin_provides() + + # integrate the id data of the binary package + bin_requires_datas = [] + bin_provides_datas = [] + for bin_package_item in self.bin_package_datas: + bin_package_key += 1 + # dependent packages + requires = self.bin_requires_dicts.get( + bin_package_item.get('pkgKey')) + if requires: + for bin_requires_item in requires: + bin_requires_item['pkgKey'] = bin_package_key + bin_requires_datas.append(bin_requires_item) + + provides = self.bin_provides_dicts.get( + bin_package_item.get('pkgKey')) + if provides: + for bin_provides_item in provides: + bin_provides_item['pkgKey'] = bin_package_key + bin_provides_datas.append(bin_provides_item) + bin_package_item['pkgKey'] = bin_package_key + # save binary package related data + try: + with DBHelper(db_name=db_file, db_type=self.db_type) as data_base: + data_base.batch_add(self.bin_package_datas, bin_package) + data_base.batch_add(bin_requires_datas, bin_requiresment) + data_base.batch_add(bin_provides_datas, bin_provides) + except SQLAlchemyError as sql_error: + LOGGER.logger.error(sql_error) + return (0, False) + else: + return (bin_package_key, True) diff --git a/packageship/packageship/application/models/package.py b/packageship/packageship/application/models/package.py new file mode 100644 index 00000000..ba65a150 --- /dev/null +++ b/packageship/packageship/application/models/package.py @@ -0,0 +1,101 @@ +''' +Database entity model mapping +''' +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from packageship.libs.dbutils.sqlalchemy_helper import DBHelper + + +class src_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + functional description:Source package model + modify record: + ''' + + __tablename__ = 'src_pack' + + id = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + version = Column(String(200), nullable=True) + + license = Column(String(500), nullable=True) + + sourceURL = Column(String(200), nullable=True) + + downloadURL = Column(String(200), nullable=True) + + Maintaniner = Column(String(50), nullable=True) + + MaintainLevel = Column(String(20), nullable=True) + + +class bin_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + functional description:Binary package data + modify record: + ''' + __tablename__ = 'bin_pack' + + id = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + version = Column(String(200), nullable=True) + + srcIDkey = Column(Integer, ForeignKey('src_pack.id')) + + src_pack = relationship('src_pack', backref="bin_pack") + + +class pack_requires(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + functional description: + modify record: + ''' + + __tablename__ = 'pack_requires' + + id = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + # depProIDkey = Column(Integer, ForeignKey( + # 'pack_provides.id'), nullable=True) + + depProIDkey = Column(Integer) + srcIDkey = Column(Integer, ForeignKey('src_pack.id'), nullable=True) + + binIDkey = Column(Integer, ForeignKey('bin_pack.id'), nullable=True) + + +class pack_provides(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + functional description: + modify record: + ''' + __tablename__ = 'pack_provides' + + id = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + binIDkey = Column(Integer, ForeignKey('bin_pack.id')) + + +class maintenance_info(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + Maintain data related to person information + ''' + __tablename__ = 'maintenance_info' + + id = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + version = Column(String(500), nullable=True) + + maintaniner = Column(String(100), nullable=True) + + maintainlevel = Column(String(100), nullable=True) diff --git a/packageship/packageship/application/models/temporarydb.py b/packageship/packageship/application/models/temporarydb.py new file mode 100644 index 00000000..9df8c3db --- /dev/null +++ b/packageship/packageship/application/models/temporarydb.py @@ -0,0 +1,85 @@ +''' +Database entity model mapping +''' +from sqlalchemy import Column, Integer, String +from packageship.libs.dbutils.sqlalchemy_helper import DBHelper + + +class src_package(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + Temporary source package model + ''' + + __tablename__ = 'src_package' + + pkgKey = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + version = Column(String(200), nullable=True) + + rpm_license = Column(String(500), nullable=True) + + url = Column(String(200), nullable=True) + + maintaniner = Column(String(100), nullable=True) + + +class bin_package(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + Temporary binary package model + ''' + __tablename__ = 'bin_package' + + pkgKey = Column(Integer, primary_key=True) + + name = Column(String(500), nullable=True) + + version = Column(String(200), nullable=True) + + rpm_license = Column(String(500), nullable=True) + + url = Column(String(500), nullable=True) + + rpm_sourcerpm = Column(String(500), nullable=True) + + src_pack_name = Column(String(200), nullable=True) + + +class src_requires(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + Temporary source package depends on package model + ''' + __tablename__ = 'src_requires' + + id = Column(Integer, primary_key=True) + + pkgKey = Column(Integer) + + name = Column(String(500), nullable=True) + + +class bin_requiresment(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + Dependency package model for temporary binary packages + ''' + __tablename__ = 'bin_requiresment' + + id = Column(Integer, primary_key=True) + + pkgKey = Column(Integer) + + name = Column(String(500), nullable=True) + + +class bin_provides(DBHelper.BASE): # pylint: disable=C0103,R0903 + ''' + Provided package model for temporary binary packages + ''' + __tablename__ = 'bin_provides' + + id = Column(Integer, primary_key=True) + + pkgKey = Column(Integer) + + name = Column(String(500), nullable=True) diff --git a/packageship/packageship/libs/__init__.py b/packageship/packageship/libs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/packageship/libs/configutils/readconfig.py b/packageship/packageship/libs/configutils/readconfig.py index c729ac61..8e2aeeae 100644 --- a/packageship/packageship/libs/configutils/readconfig.py +++ b/packageship/packageship/libs/configutils/readconfig.py @@ -3,7 +3,6 @@ which mainly includes obtaining specific node values and obtaining arbitrary node values ''' -import os import configparser from configparser import NoSectionError from configparser import NoOptionError @@ -30,6 +29,7 @@ class ReadConfig: return None except NoOptionError: return None + return None def get_database(self, param): ''' @@ -42,6 +42,7 @@ class ReadConfig: return None except NoOptionError: return None + return None def get_config(self, node, param): ''' @@ -54,3 +55,4 @@ class ReadConfig: return None except NoOptionError: return None + return None diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index 966aa6bd..00e26d42 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -3,7 +3,6 @@ Simple encapsulation of sqlalchemy orm framework operation database ''' import os -from packageship.system_config import DATABASE_FOLDER_PATH from sqlalchemy import create_engine from sqlalchemy import MetaData from sqlalchemy.orm import sessionmaker @@ -15,15 +14,18 @@ from packageship.libs.exception.ext import Error from packageship.libs.exception.ext import DbnameNoneException from packageship.libs.exception.ext import ContentNoneException from packageship.libs.configutils.readconfig import ReadConfig +from packageship.system_config import DATABASE_FOLDER_PATH class DBHelper(): - + ''' + Database connection, operation public class + ''' # The base class inherited by the data model BASE = declarative_base() - def __init__(self, user_name=None, passwrod=None, ip_address=None, \ - port=None, db_name=None, db_type=None, *args, **kwargs): + def __init__(self, user_name=None, passwrod=None, ip_address=None, # pylint: disable=R0913 + port=None, db_name=None, db_type=None, **kwargs): self.user_name = user_name self._readconfig = ReadConfig() if self.user_name is None: @@ -61,6 +63,13 @@ class DBHelper(): self._db_file_path() self.db_name = os.path.join( self.database_file_path, self.db_name + '.db') + self._create_engine() + self.session = None + + def _create_engine(self): + ''' + Create a database connection object + ''' if self.db_type.startswith('sqlite'): if not self.db_name: raise DbnameNoneException( @@ -75,13 +84,12 @@ class DBHelper(): 'password': self.passwrod, 'host': self.ip_address, 'port': self.port, - 'drivername': self.db_type}), \ - encoding='utf-8', \ - convert_unicode=True) + 'drivername': self.db_type}), + encoding='utf-8', + convert_unicode=True) else: raise DisconnectionError( 'A disconnect is detected on a raw DB-API connection') - self.session = None def _db_file_path(self): ''' @@ -99,12 +107,12 @@ class DBHelper(): functional description:Create a context manager for the database connection ''' - Session = sessionmaker() + session = sessionmaker() if getattr(self, 'engine') is None: raise DisconnectionError('Abnormal database connection') - Session.configure(bind=self.engine) + session.configure(bind=self.engine) - self.session = Session() + self.session = session() return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -132,7 +140,6 @@ class DBHelper(): ''' meta = MetaData(self.engine) for table_name in DBHelper.BASE.metadata.tables.keys(): - from sqlalchemy import Table if table_name in tables: table = DBHelper.BASE.metadata.tables[table_name] table.metadata = meta @@ -153,8 +160,8 @@ class DBHelper(): try: self.session.add(entity) - except SQLAlchemyError as e: - raise Error(e) + except SQLAlchemyError as sql_error: + raise Error(sql_error) else: self.session.commit() return entity @@ -183,7 +190,7 @@ class DBHelper(): model.__table__.insert(), dicts ) - except SQLAlchemyError as e: - raise Error(e) + except SQLAlchemyError as sql_error: + raise Error(sql_error) else: self.session.commit() diff --git a/packageship/packageship/libs/exception/ext.py b/packageship/packageship/libs/exception/ext.py index b4ff3bdd..12a96ef6 100644 --- a/packageship/packageship/libs/exception/ext.py +++ b/packageship/packageship/libs/exception/ext.py @@ -1,6 +1,12 @@ +''' + System exception information +''' + class Error(Exception): - """Base class for ConfigParser exceptions.""" + """ + Base class for ConfigParser exceptions + """ def __init__(self, msg=''): self.message = msg diff --git a/packageship/packageship/libs/log/loghelper.py b/packageship/packageship/libs/log/loghelper.py index b4b90c3f..d5efe129 100644 --- a/packageship/packageship/libs/log/loghelper.py +++ b/packageship/packageship/libs/log/loghelper.py @@ -1,3 +1,6 @@ +''' +Logging related +''' import os import pathlib import logging @@ -8,12 +11,12 @@ from packageship.libs.configutils.readconfig import ReadConfig READCONFIG = ReadConfig() -def setup_log(Config=None): +def setup_log(config=None): ''' Log logging in the context of flask ''' - if Config: - logging.basicConfig(level=Config.LOG_LEVEL) + if config: + logging.basicConfig(level=config.LOG_LEVEL) else: _level = READCONFIG.get_config('LOG', 'log_level') if _level is None: @@ -28,7 +31,7 @@ def setup_log(Config=None): if not os.path.exists(path): try: os.makedirs(os.path.split(path)[0]) - except FileExistsError as file_exists: + except FileExistsError: pathlib.Path(path).touch() file_log_handler = RotatingFileHandler( @@ -43,6 +46,9 @@ def setup_log(Config=None): class Log(): + ''' + General log operations + ''' def __init__(self, name=__name__, path=None): self.__name = name @@ -86,14 +92,20 @@ class Log(): # self.__stream_handler.setFormatter(formatter) self.__file_handler.setFormatter(formatter) - def __close_handler(self): + def close_handler(self): + ''' + Turn off log processing + ''' # self.__stream_handler.close() self.__file_handler.close() @property def logger(self): + ''' + Get logs + ''' self.__ini_handler() self.__set_handler() self.__set_formatter() - self.__close_handler() + self.close_handler() return self.__logger -- Gitee From 30ab2585025e89f0dc6ed186dd68d18b64dda810 Mon Sep 17 00:00:00 2001 From: gongzt Date: Tue, 23 Jun 2020 13:39:37 +0800 Subject: [PATCH 16/72] Rename of cli file --- packageship/packageship/{cli.py => pkgship.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packageship/packageship/{cli.py => pkgship.py} (100%) diff --git a/packageship/packageship/cli.py b/packageship/packageship/pkgship.py similarity index 100% rename from packageship/packageship/cli.py rename to packageship/packageship/pkgship.py -- Gitee From e2d3fcb3251d547aec571feac3d6bd843e4a0460 Mon Sep 17 00:00:00 2001 From: gongzt Date: Tue, 23 Jun 2020 18:27:51 +0800 Subject: [PATCH 17/72] Change the setting configuration file name --- packageship/packageship/application/{setting.py => settings.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packageship/packageship/application/{setting.py => settings.py} (100%) diff --git a/packageship/packageship/application/setting.py b/packageship/packageship/application/settings.py similarity index 100% rename from packageship/packageship/application/setting.py rename to packageship/packageship/application/settings.py -- Gitee From 10103bc9fdc7e6d6d9f07a930721d96f2b70c451 Mon Sep 17 00:00:00 2001 From: gongzt Date: Wed, 24 Jun 2020 09:34:33 +0800 Subject: [PATCH 18/72] Optimized configuration file for flask framework startup --- packageship/packageship/application/settings.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packageship/packageship/application/settings.py b/packageship/packageship/application/settings.py index 743d3db8..59462373 100644 --- a/packageship/packageship/application/settings.py +++ b/packageship/packageship/application/settings.py @@ -1,6 +1,8 @@ -from packageship.libs.configutils.readconfig import ReadConfig +''' + Basic configuration of flask framework +''' import random -import string +from packageship.libs.configutils.readconfig import ReadConfig class Config(): @@ -13,7 +15,7 @@ class Config(): LOG_LEVEL = 'INFO' - def __init__(self, *args, **kwargs): + def __init__(self): self._read_config = ReadConfig() -- Gitee From 012c0294ef4244e74df98cfeba83f613689f6a43 Mon Sep 17 00:00:00 2001 From: gongzt Date: Wed, 24 Jun 2020 10:13:37 +0800 Subject: [PATCH 19/72] Remove the commented code --- .../packageship/application/initsystem/data_import.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index e264ecb6..af2b00ef 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -252,12 +252,6 @@ class InitDataBase(): self._bin_package_dicts[src_package_name][index]['relation'] = False self._bin_package_dicts[src_package_name].append(_bin_package) - # self._bin_package_dicts[src_package_name].append(_bin_package) - # self._bin_package_dicts[src_package_name].sort( - # key=lambda x: x.get('version')) - # self._bin_package_dicts[src_package_name][-1]['relation'] = True - # for bin_package_item in self._bin_package_dicts[src_package_name][:-1]: - # bin_package_item['relation'] = False def _bin_requires_relation(self, bin_requires_data): ''' -- Gitee From 2829cc0fbf16001522386f72c1be45d0f8fba27d Mon Sep 17 00:00:00 2001 From: jiang Date: Wed, 24 Jun 2020 16:29:54 +0800 Subject: [PATCH 20/72] initsystem and packages and singlepackages and repodata interfaces --- .../apps/package/function/constants.py | 78 ++++++ .../apps/package/function/packages.py | 164 ++++++++++++ .../apps/package/function/searchdb.py | 30 +++ .../application/apps/package/serialize.py | 172 ++++++++++++ .../application/apps/package/view.py | 248 +++++++++++++++++- 5 files changed, 680 insertions(+), 12 deletions(-) create mode 100644 packageship/packageship/application/apps/package/function/constants.py create mode 100644 packageship/packageship/application/apps/package/function/packages.py create mode 100644 packageship/packageship/application/apps/package/function/searchdb.py create mode 100644 packageship/packageship/application/apps/package/serialize.py diff --git a/packageship/packageship/application/apps/package/function/constants.py b/packageship/packageship/application/apps/package/function/constants.py new file mode 100644 index 00000000..933ec27e --- /dev/null +++ b/packageship/packageship/application/apps/package/function/constants.py @@ -0,0 +1,78 @@ +''' + Response contain and code ID +''' + + +class ListNode: + """ + the structure of dict: + {package_name: [source_name, + dbname, + [[parent_node_1, depend_type],[parent_node_2, depend_type],...]], + check_tag] + } + """ + + SOURCE_NAME = 0 + VERSION = 1 + DBNAME = 2 + PARENT_LIST = 3 + # FOR PARENT LIST: + PARENT_NODE = 0 + DEPEND_TYPE = 1 + +# response code + + +class ResponseCode(): + """ + Description: response code to web + changeLog: + """ + + SUCCESS = "2001" + PARAM_ERROR = "4001" + DB_NAME_ERROR = "4002" + PACK_NAME_NOT_FOUND = "4003" + CONNECT_DB_ERROR = "4004" + FILE_NOT_FOUND = "40041" + + INPUT_NONE = "4005" + DELETE_DB_ERROR = "40051" + + CONFIGFILE_PATH_EMPTY = "50001" + FAILED_CREATE_DATABASE_TABLE = "50002" + TYPEERROR = "50003" + DATAMERGEERROR = "50004" + FILE_NOT_FIND_ERROR = "50005" + DIS_CONNECTION_DB = "50006" + + CODE_MSG_MAP = { + SUCCESS: "Successful Operation!", + PARAM_ERROR: "Parameter error, please check the parameter and query again.", + DB_NAME_ERROR: "Database does not exist! Please check the database name", + PACK_NAME_NOT_FOUND: "Sorry! The querying package does not exist in the databases", + CONNECT_DB_ERROR: "Failed to Connect the database! " + "Please check the database connection", + FILE_NOT_FOUND: "Database import success file does not exist", + DELETE_DB_ERROR: "Failed to delete database", + CONFIGFILE_PATH_EMPTY: "Initialization profile does not exist or cannot be found", + FAILED_CREATE_DATABASE_TABLE: "Failed to create database or table", + TYPEERROR: "The source code and binary path types in the initialization file are abnormal", + DATAMERGEERROR: "abnormal multi-file database integration", + FILE_NOT_FIND_ERROR: "system initialization configuration file does not exist", + DIS_CONNECTION_DB: "Unable to connect to the database, check the database configuration"} + + @classmethod + def response_json(cls, code, data=None): + """ + classmethod + """ + return { + "code": code, + "msg": cls.CODE_MSG_MAP[code], + "data": data + } + + def __str__(self): + return 'ResponseCode' diff --git a/packageship/packageship/application/apps/package/function/packages.py b/packageship/packageship/application/apps/package/function/packages.py new file mode 100644 index 00000000..a4013cff --- /dev/null +++ b/packageship/packageship/application/apps/package/function/packages.py @@ -0,0 +1,164 @@ +""" +docstring:Get package information and modify package information +""" +from flask import current_app + +from packageship.libs.dbutils import DBHelper +from packageship.application.models.package import src_pack +from packageship.application.models.package import pack_provides +from packageship.application.models.package import maintenance_info +from packageship.application.models.package import pack_requires +from packageship.application.models.package import bin_pack +from packageship.libs.exception import Error + + +def get_packages(dbname): + ''' + Description: Get all packages info + :return: package info + ''' + with DBHelper(db_name=dbname) as db_name: + src_pack_queryset = db_name.session.query(src_pack).all() + resp_list = [] + for src_pack_obj in src_pack_queryset: + package = {} + package["sourceName"] = src_pack_obj.name + package["version"] = src_pack_obj.version + package["license"] = src_pack_obj.license + package["maintainer"] = src_pack_obj.Maintaniner + package["maintainlevel"] = src_pack_obj.MaintainLevel + package["sourceURL"] = src_pack_obj.sourceURL + package["maintainlevel"] = src_pack_obj.MaintainLevel + package["downloadURL"] = src_pack_obj.downloadURL + package["dbname"] = dbname + resp_list.append(package) + return resp_list + + +def buildep_packages(dbname, src_pack_id): + """ + :param dbname: databases name + :param src_pack_id: srcpackage id + :return: buildDep Compile dependencies of source packages + """ + with DBHelper(db_name=dbname) as db_name: + b_pack_requires_set = db_name.session.query( + pack_requires).filter_by(srcIDkey=src_pack_id).all() + b_dep_proid_keys = [ + dep_proid_obj.depProIDkey for dep_proid_obj in b_pack_requires_set] + b_pack_pro_set = db_name.session.query(pack_provides).filter( + pack_provides.id.in_(b_dep_proid_keys)).all() + b_bin_pack_ids = [ + bin_pack_obj.binIDkey for bin_pack_obj in b_pack_pro_set] + b_bin_pack_set = db_name.session.query(bin_pack).filter( + bin_pack.id.in_(b_bin_pack_ids)).all() + builddep = [bin_pack_obj.name for bin_pack_obj in b_bin_pack_set] + return builddep + + +def sub_packages(dbname, src_pack_id): + """ + :param dbname: databases name + :param src_pack_id: srcpackage id + :return: subpack Source package to binary package, then find the installation dependencies + of the binary package + """ + with DBHelper(db_name=dbname) as db_name: + subpack = {} + i_bin_pack_set = db_name.session.query( + bin_pack).filter_by(srcIDkey=src_pack_id).all() + i_bin_pack_ids = [ + bin_pack_obj.id for bin_pack_obj in i_bin_pack_set] + for i_bin_pack_id in i_bin_pack_ids: + i_bin_pack_name = db_name.session.query( + bin_pack).filter_by(id=i_bin_pack_id).first().name + i_pack_req_set = db_name.session.query( + pack_requires).filter_by(binIDkey=i_bin_pack_id).all() + i_dep_proid_keys = [ + dep_proid_obj.depProIDkey for dep_proid_obj in i_pack_req_set] + i_dep_proid_keys = list(set(i_dep_proid_keys)) + i_pack_provides_set = db_name.session.query(pack_provides).filter( + pack_provides.id.in_(i_dep_proid_keys)).all() + i_bin_pack_ids = [ + bin_pack_obj.binIDkey for bin_pack_obj in i_pack_provides_set] + i_bin_pack_set = db_name.session.query(bin_pack).filter( + bin_pack.id.in_(i_bin_pack_ids)).all() + i_bin_pack_names = [ + bin_pack_obj.name for bin_pack_obj in i_bin_pack_set] + subpack[i_bin_pack_name] = i_bin_pack_names + return subpack + + +def get_single_package(dbname, sourcename): + ''' + Description: Get all packages info + :return: package info + ''' + with DBHelper(db_name=dbname) as db_name: + package = {} + src_pack_obj = db_name.session.query(src_pack).filter_by( + name=sourcename).first() + package["sourceName"] = src_pack_obj.name + package["version"] = src_pack_obj.version + package["license"] = src_pack_obj.license + package["maintainer"] = src_pack_obj.Maintaniner + package["maintainlevel"] = src_pack_obj.MaintainLevel + package["sourceURL"] = src_pack_obj.sourceURL + package["downloadURL"] = src_pack_obj.downloadURL + package["dbname"] = dbname + src_pack_id = src_pack_obj.id + builddep = buildep_packages(dbname, src_pack_id) + subpack = sub_packages(dbname, src_pack_id) + package['buildDep'] = builddep + package['subpack'] = subpack + return package + + +def update_single_package( + package_name, + dbname, + maintainer, + maintain_level): + """ + change single package management + :return: message success or failed + """ + with DBHelper(db_name=dbname) as db_name: + update_obj = db_name.session.query( + src_pack).filter_by(name=package_name).first() + update_obj.Maintaniner = maintainer + update_obj.MaintainLevel = maintain_level + db_name.session.commit() + + +def update_maintaniner_info(package_name, + dbname, + maintaniner, + maintainlevel): + ''' + Separate maintenance information + ''' + with DBHelper(db_name=dbname) as db_name: + src_pack_obj = db_name.session.query(src_pack).filter_by( + name=package_name).first() + name = src_pack_obj.name + version = src_pack_obj.version + with DBHelper(db_name='maintenance.information') as dbs_name: + try: + information_obj = dbs_name.session.query(maintenance_info).filter_by( + name=package_name, version=version).first() + if information_obj is None: + information = maintenance_info( + name=name, + version=version, + maintaniner=maintaniner, + maintainlevel=maintainlevel) + dbs_name.session.add(information) + dbs_name.session.commit() + else: + information_obj.maintaniner = maintaniner + information_obj.maintainlevel = maintainlevel + dbs_name.session.commit() + except (AttributeError, Error) as attri_error: + current_app.logger.error(attri_error) + return diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py new file mode 100644 index 00000000..374456f8 --- /dev/null +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -0,0 +1,30 @@ +""" + A set for all query databases function +""" + +import yaml +from flask import current_app + +from packageship.libs.exception import ContentNoneException, Error +from packageship.system_config import DATABASE_SUCCESS_FILE + + +def db_priority(): + """ + return dbprioty + """ + try: + with open(DATABASE_SUCCESS_FILE, 'r', encoding='utf-8') as file_context: + + init_database_date = yaml.load( + file_context.read(), Loader=yaml.FullLoader) + if init_database_date is None: + raise ContentNoneException( + "The content of the database initialization configuration file cannot be empty") + init_database_date.sort(key=lambda x: x['priority'], reverse=False) + db_list = [item.get('database_name') + for item in init_database_date] + return db_list + except (FileNotFoundError, Error) as file_not_found: + current_app.logger.error(file_not_found) + return None diff --git a/packageship/packageship/application/apps/package/serialize.py b/packageship/packageship/application/apps/package/serialize.py new file mode 100644 index 00000000..dbc4735d --- /dev/null +++ b/packageship/packageship/application/apps/package/serialize.py @@ -0,0 +1,172 @@ +""" +marshmallow serialize +""" +from marshmallow import Schema +from marshmallow import fields +from marshmallow import ValidationError +from marshmallow import validate + + +class PackagesSchema(Schema): + """ + PackagesSchema serialize + """ + dbName = fields.Str(validate=validate.Length( + max=50), required=False, allow_none=True) + + +class GetpackSchema(Schema): + """ + GetpackSchema serialize + """ + sourceName = fields.Str( + required=True, + validate=validate.Length(min=1, + max=200)) + + dbName = fields.Str(validate=validate.Length( + max=30), required=False, allow_none=True) + version = fields.Str(validate=validate.Length( + max=200), required=False, allow_none=True) + + +def validate_maintainlevel(maintainlevel): + """ + Method test + """ + if maintainlevel not in ['1', '2', '3', '4']: + raise ValidationError("maintainLevel is illegal data ") + + +class PutpackSchema(Schema): + """ + PutpackSchema serialize + """ + sourceName = fields.Str( + required=True, + validate=validate.Length( + min=1, + max=200)) + dbName = fields.Str( + required=True, + validate=validate.Length( + min=1, + max=50)) + maintainer = fields.Str(validate=validate.Length( + max=50), required=False, allow_none=True) + maintainlevel = fields.Str( + validate=validate_maintainlevel, + required=False, + allow_none=True) + + +class InstallDependSchema(Schema): + """ + InstallDependSchema + """ + binaryName = fields.Str( + required=True, + validate=validate.Length( + min=1, max=500)) + db_list = fields.List(fields.String(), required=False, allow_none=True) + + +class BuildDependSchema(Schema): + """ + BuildDependSchema serialize + """ + sourceName = fields.Str( + required=True, + validate=validate.Length( + min=1, max=200)) + db_list = fields.List(fields.String(), required=False, allow_none=True) + + +def validate_withsubpack(withsubpack): + """ + Method test + """ + if withsubpack not in ['0', '1']: + raise ValidationError("withSubpack is illegal data ") + + +class BeDependSchema(Schema): + """ + BeDependSchema serialize + """ + packagename = fields.Str( + required=True, + validate=validate.Length( + min=1, + max=200)) + withsubpack = fields.Str( + validate=validate_withsubpack, + required=False, allow_none=True) + dbname = fields.Str( + required=True, + validate=validate.Length( + min=1, + max=50)) + + +def validate_selfbuild(selfbuild): + """ + Method test + """ + if selfbuild not in ['0', '1']: + raise ValidationError("selfbuild is illegal data ") + + +def validate_packtype(packtype): + """ + Method test + """ + if packtype not in ['source', 'binary']: + raise ValidationError("packtype is illegal data ") + + +class SelfDependSchema(Schema): + """ + SelfDependSchema serialize + """ + packagename = fields.Str( + required=True, + validate=validate.Length( + min=1, + max=200)) + db_list = fields.List(fields.String(), required=False, allow_none=True) + selfbuild = fields.Str(validate=validate_selfbuild, + required=False, allow_none=True) + withsubpack = fields.Str( + validate=validate_withsubpack, required=False, allow_none=True) + packtype = fields.Str(validate=validate_packtype, + required=False, allow_none=True) + + +class DeletedbSchema(Schema): + """ + DeletedbSchema serialize + """ + dbName = fields.Str( + required=True, + validate=validate.Length( + min=1, + max=200)) + + +def have_err_db_name(db_list, db_priority): + ''' + @param:db_list db list of inputs + @param:db_priority default list + return:If any element in db_list is no longer in db_priority, return false + ''' + return any(filter(lambda db_name: db_name not in db_priority, db_list)) + + +class InitSystemSchema(Schema): + """ + InitSystemSchema serialize + """ + configfile = fields.Str( + validate=validate.Length( + max=50), required=False, allow_none=True) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 16a76b27..1005eb39 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -1,13 +1,35 @@ +""" +view: Request logic processing Return json format +""" +import yaml from flask import request from flask_restful import Resource from flask import jsonify from flask import current_app -from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.exc import DisconnectionError +from packageship.application.apps.package.function.constants import ResponseCode +from packageship.application.apps.package.function.packages import get_packages +from packageship.application.apps.package.function.packages import update_single_package +from packageship.application.apps.package.function.packages import update_maintaniner_info +from packageship.application.apps.package.function.packages import get_single_package +from packageship.application.apps.package.function.searchdb import db_priority +from packageship.application.apps.package.serialize import PackagesSchema +from packageship.application.apps.package.serialize import GetpackSchema +from packageship.application.apps.package.serialize import PutpackSchema +from packageship.application.apps.package.serialize import DeletedbSchema +from packageship.application.apps.package.serialize import InitSystemSchema +from packageship.application.initsystem.data_import import InitDataBase +from packageship.libs.configutils.readconfig import ReadConfig +from packageship.libs.exception import Error +from packageship.libs.exception import ContentNoneException +from packageship.libs.exception import DataMergeException from packageship.libs.log import Log +from packageship.system_config import DATABASE_SUCCESS_FILE LOGGER = Log(__name__) + class Packages(Resource): ''' Description: interface for package info management @@ -15,7 +37,7 @@ class Packages(Resource): changeLog: ''' - def get(self, *args, **kwargs): + def get(self): ''' Description: Get all package info from a database input: @@ -25,7 +47,44 @@ class Packages(Resource): Exception: Changelog: ''' - pass + # Get verification parameters + schema = PackagesSchema() + data = schema.dump(request.args) + if schema.validate(data): + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + dbname = data.get("dbName", None) + # Call method to query + try: + dbpreority = db_priority() + if dbpreority is None: + return jsonify( + ResponseCode.response_json(ResponseCode.FILE_NOT_FOUND) + ) + if not dbname: + response = [] + for dbname in dbpreority: + query_result = get_packages(dbname) + for item in query_result: + response.append(item) + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS, response) + ) + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + # Database queries data and catches exceptions + except DisconnectionError as dis_connection_error: + current_app.logger.error(dis_connection_error) + return jsonify( + ResponseCode.response_json( + ResponseCode.DIS_CONNECTION_DB)) + except (AttributeError, Error) as attri_error: + current_app.logger.error(attri_error) + return jsonify( + ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) + ) class SinglePack(Resource): @@ -35,7 +94,7 @@ class SinglePack(Resource): ChangeLog: ''' - def get(self, *args, **kwargs): + def get(self): ''' description: Searching a package info input: @@ -46,9 +105,52 @@ class SinglePack(Resource): exception: changeLog: ''' - pass + # Get verification parameters + schema = GetpackSchema() + data = schema.dump(request.args) + if schema.validate(data): + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + dbname = data.get("dbName", None) + sourcename = data.get("sourceName") + + # Call method to query + try: + dbpreority = db_priority() + if db_priority is None: + return jsonify( + ResponseCode.response_json(ResponseCode.FILE_NOT_FOUND) + ) + if not dbname: + response = [] + for dbname in dbpreority: + query_result = get_single_package(dbname, sourcename) + response.append(query_result) + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS, response) + ) - def put(self, *args, **kwargs): + # Database queries data and catches exceptions + if dbname not in dbpreority: + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + response = get_single_package(dbname, sourcename) + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS, [response]) + ) + except DisconnectionError as dis_connection_error: + current_app.logger.error(dis_connection_error) + abnormal = ResponseCode.DIS_CONNECTION_DB + + except (AttributeError, Error) as attribute_error: + current_app.logger.error(attribute_error) + abnormal = ResponseCode.PACK_NAME_NOT_FOUND + if abnormal is not None: + return jsonify(ResponseCode.response_json(abnormal)) + + def put(self): ''' Description: update a package info input: @@ -60,7 +162,47 @@ class SinglePack(Resource): exception: changeLog: ''' - pass + # Get verification parameters + schema = PutpackSchema() + data = schema.dump(request.get_json()) + if schema.validate(data): + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + dbname = data.get('dbName') + sourcename = data.get('sourceName') + maintainer = data.get('maintainer', None) + maintain_level = data.get('maintainlevel', None) + + # Call method to query + if not maintainer and not maintain_level: + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + + if dbname not in db_priority(): + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + # Database queries data and catches exceptions + try: + update_single_package( + sourcename, dbname, maintainer, maintain_level) + update_maintaniner_info( + sourcename, dbname, maintainer, maintain_level) + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS) + ) + except DisconnectionError as dis_connection_error: + current_app.logger.error(dis_connection_error) + return jsonify( + ResponseCode.response_json( + ResponseCode.DIS_CONNECTION_DB)) + except (AttributeError, Error) as attri_error: + current_app.logger.error(attri_error) + return jsonify( + ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) + ) class InstallDepend(Resource): @@ -102,6 +244,7 @@ class BuildDepend(Resource): Restful API: post changeLog: ''' + def post(self, *args, **kwargs): ''' Description: Query a package's build depend and @@ -116,7 +259,7 @@ class BuildDepend(Resource): binaryName: srcName: dbName: - type: install or build, which depend + type: install or build, which depend on the function parentNode: the binary package name which is the build/install depend for binaryName @@ -199,7 +342,7 @@ class BeDepend(Resource): class Repodatas(Resource): """API for operating databases""" - def get(self, *args, **kwargs): + def get(self): ''' description: get all database input: @@ -210,14 +353,60 @@ class Repodatas(Resource): exception: changeLog: ''' - pass + try: + with open(DATABASE_SUCCESS_FILE, 'r', encoding='utf-8') as file_context: + init_database_date = yaml.load( + file_context.read(), Loader=yaml.FullLoader) + if init_database_date is None: + raise ContentNoneException( + "The content of the database initialization configuration " + "file cannot be empty") + init_database_date.sort( + key=lambda x: x['priority'], reverse=False) + return jsonify( + ResponseCode.response_json( + ResponseCode.SUCCESS, + data=init_database_date)) + except (FileNotFoundError, Error) as file_not_found: + current_app.logger.error(file_not_found) + return jsonify( + ResponseCode.response_json(ResponseCode.FILE_NOT_FOUND) + ) def delete(self, *args, **kwargs): ''' description: get all database input: database name - return: + return: success or failure ''' + schema = DeletedbSchema() + data = schema.dump(request.args) + if schema.validate(data): + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + db_name = data.get("dbName") + db_list = db_priority() + if db_list is None: + return jsonify( + ResponseCode.response_json(ResponseCode.FILE_NOT_FOUND) + ) + if db_name not in db_priority(): + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + try: + drop_db = InitDataBase() + drop_db.delete_db(db_name) + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS) + ) + except Error as error: + current_app.logger.error(error) + return jsonify( + ResponseCode.response_json(ResponseCode.DELETE_DB_ERROR) + ) + class InitSystem(Resource): '''InitSystem''' @@ -230,5 +419,40 @@ class InitSystem(Resource): exception: changeLog: """ - pass + schema = InitSystemSchema() + data = request.get_json() + validate_err = schema.validate(data) + if validate_err: + return jsonify( + ResponseCode.response_json( + ResponseCode.PARAM_ERROR)) + configfile = data.get("configfile", None) + try: + abnormal = None + if not configfile: + _config_path = ReadConfig().get_system('init_conf_path') + InitDataBase(config_file_path=_config_path).init_data() + else: + InitDataBase(config_file_path=configfile).init_data() + except ContentNoneException as content_none_error: + LOGGER.logger.error(content_none_error) + abnormal = ResponseCode.CONFIGFILE_PATH_EMPTY + except DisconnectionError as dis_connection_error: + LOGGER.logger.error(dis_connection_error) + abnormal = ResponseCode.DIS_CONNECTION_DB + except TypeError as type_error: + LOGGER.logger.error(type_error) + abnormal = ResponseCode.TYPEERROR + except DataMergeException as data_merge_error: + LOGGER.logger.error(data_merge_error) + abnormal = ResponseCode.DATAMERGEERROR + except FileNotFoundError as file_not_found_error: + LOGGER.logger.error(file_not_found_error) + abnormal = ResponseCode.FILE_NOT_FIND_ERROR + except Error as error: + LOGGER.logger.error(error) + abnormal = ResponseCode.FAILED_CREATE_DATABASE_TABLE + if abnormal is not None: + return jsonify(ResponseCode.response_json(abnormal)) + return jsonify(ResponseCode.response_json(ResponseCode.SUCCESS)) -- Gitee From 11e894a3f0fb25c64acb1e8adeb6c275704c135f Mon Sep 17 00:00:00 2001 From: jiang Date: Sun, 28 Jun 2020 08:49:14 +0800 Subject: [PATCH 21/72] Clear status code information and perfect exception capture in the interface --- .../apps/package/function/constants.py | 16 +++++++-------- .../application/apps/package/view.py | 20 +++++++++---------- .../application/initsystem/data_import.py | 1 + 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/constants.py b/packageship/packageship/application/apps/package/function/constants.py index 933ec27e..59a2b4af 100644 --- a/packageship/packageship/application/apps/package/function/constants.py +++ b/packageship/packageship/application/apps/package/function/constants.py @@ -29,21 +29,21 @@ class ResponseCode(): Description: response code to web changeLog: """ - + # Four digits are common status codes SUCCESS = "2001" PARAM_ERROR = "4001" DB_NAME_ERROR = "4002" PACK_NAME_NOT_FOUND = "4003" CONNECT_DB_ERROR = "4004" - FILE_NOT_FOUND = "40041" - INPUT_NONE = "4005" + # Delete database module error status code + FILE_NOT_FOUND = "40041" DELETE_DB_ERROR = "40051" - + # Database initialization module error status code CONFIGFILE_PATH_EMPTY = "50001" FAILED_CREATE_DATABASE_TABLE = "50002" - TYPEERROR = "50003" - DATAMERGEERROR = "50004" + TYPE_ERROR = "50003" + DATA_MERGE_ERROR = "50004" FILE_NOT_FIND_ERROR = "50005" DIS_CONNECTION_DB = "50006" @@ -58,8 +58,8 @@ class ResponseCode(): DELETE_DB_ERROR: "Failed to delete database", CONFIGFILE_PATH_EMPTY: "Initialization profile does not exist or cannot be found", FAILED_CREATE_DATABASE_TABLE: "Failed to create database or table", - TYPEERROR: "The source code and binary path types in the initialization file are abnormal", - DATAMERGEERROR: "abnormal multi-file database integration", + TYPE_ERROR: "The source code and binary path types in the initialization file are abnormal", + DATA_MERGE_ERROR: "abnormal multi-file database integration", FILE_NOT_FIND_ERROR: "system initialization configuration file does not exist", DIS_CONNECTION_DB: "Unable to connect to the database, check the database configuration"} diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 1005eb39..b66cb916 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -3,9 +3,9 @@ view: Request logic processing Return json format """ import yaml from flask import request -from flask_restful import Resource from flask import jsonify from flask import current_app +from flask_restful import Resource from sqlalchemy.exc import DisconnectionError from packageship.application.apps.package.function.constants import ResponseCode @@ -144,7 +144,7 @@ class SinglePack(Resource): current_app.logger.error(dis_connection_error) abnormal = ResponseCode.DIS_CONNECTION_DB - except (AttributeError, Error) as attribute_error: + except (AttributeError, TypeError, Error) as attribute_error: current_app.logger.error(attribute_error) abnormal = ResponseCode.PACK_NAME_NOT_FOUND if abnormal is not None: @@ -198,7 +198,7 @@ class SinglePack(Resource): return jsonify( ResponseCode.response_json( ResponseCode.DIS_CONNECTION_DB)) - except (AttributeError, Error) as attri_error: + except (AttributeError, TypeError, Error) as attri_error: current_app.logger.error(attri_error) return jsonify( ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) @@ -367,13 +367,13 @@ class Repodatas(Resource): ResponseCode.response_json( ResponseCode.SUCCESS, data=init_database_date)) - except (FileNotFoundError, Error) as file_not_found: + except (FileNotFoundError, TypeError, Error) as file_not_found: current_app.logger.error(file_not_found) return jsonify( ResponseCode.response_json(ResponseCode.FILE_NOT_FOUND) ) - def delete(self, *args, **kwargs): + def delete(self): ''' description: get all database input: database name @@ -401,7 +401,7 @@ class Repodatas(Resource): return jsonify( ResponseCode.response_json(ResponseCode.SUCCESS) ) - except Error as error: + except (FileNotFoundError, TypeError, Error) as error: current_app.logger.error(error) return jsonify( ResponseCode.response_json(ResponseCode.DELETE_DB_ERROR) @@ -411,7 +411,7 @@ class Repodatas(Resource): class InitSystem(Resource): '''InitSystem''' - def post(self, *args, **kwargs): + def post(self): """ description: InitSystem input: @@ -443,14 +443,14 @@ class InitSystem(Resource): abnormal = ResponseCode.DIS_CONNECTION_DB except TypeError as type_error: LOGGER.logger.error(type_error) - abnormal = ResponseCode.TYPEERROR + abnormal = ResponseCode.TYPE_ERROR except DataMergeException as data_merge_error: LOGGER.logger.error(data_merge_error) - abnormal = ResponseCode.DATAMERGEERROR + abnormal = ResponseCode.DATA_MERGE_ERROR except FileNotFoundError as file_not_found_error: LOGGER.logger.error(file_not_found_error) abnormal = ResponseCode.FILE_NOT_FIND_ERROR - except Error as error: + except (Error, Exception) as error: LOGGER.logger.error(error) abnormal = ResponseCode.FAILED_CREATE_DATABASE_TABLE if abnormal is not None: diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index af2b00ef..eebd0969 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -139,6 +139,7 @@ class InitDataBase(): database:database configuration information ''' try: + db_file = None # 1. create a database and related tables in the database db_name = self._create_database(database) # 2. get the data of binary packages and source packages -- Gitee From 1935109124edc5de876e0c89ec20550a7eaebb88 Mon Sep 17 00:00:00 2001 From: jiang Date: Sun, 28 Jun 2020 12:02:33 +0800 Subject: [PATCH 22/72] Clear status code information and Improve interface input and participate in input and output and Remove exception --- .../apps/package/function/constants.py | 5 ++--- .../apps/package/function/packages.py | 20 +++++++++++++++++-- .../application/apps/package/view.py | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/constants.py b/packageship/packageship/application/apps/package/function/constants.py index 59a2b4af..09a7bfa3 100644 --- a/packageship/packageship/application/apps/package/function/constants.py +++ b/packageship/packageship/application/apps/package/function/constants.py @@ -36,10 +36,9 @@ class ResponseCode(): PACK_NAME_NOT_FOUND = "4003" CONNECT_DB_ERROR = "4004" INPUT_NONE = "4005" - # Delete database module error status code - FILE_NOT_FOUND = "40041" + FILE_NOT_FOUND = "4006" + # Database operation module error status code DELETE_DB_ERROR = "40051" - # Database initialization module error status code CONFIGFILE_PATH_EMPTY = "50001" FAILED_CREATE_DATABASE_TABLE = "50002" TYPE_ERROR = "50003" diff --git a/packageship/packageship/application/apps/package/function/packages.py b/packageship/packageship/application/apps/package/function/packages.py index a4013cff..25a0508e 100644 --- a/packageship/packageship/application/apps/package/function/packages.py +++ b/packageship/packageship/application/apps/package/function/packages.py @@ -15,6 +15,7 @@ from packageship.libs.exception import Error def get_packages(dbname): ''' Description: Get all packages info + :param dbname: Database name :return: package info ''' with DBHelper(db_name=dbname) as db_name: @@ -37,6 +38,7 @@ def get_packages(dbname): def buildep_packages(dbname, src_pack_id): """ + Description: Query package layer 1 compilation dependency :param dbname: databases name :param src_pack_id: srcpackage id :return: buildDep Compile dependencies of source packages @@ -58,6 +60,7 @@ def buildep_packages(dbname, src_pack_id): def sub_packages(dbname, src_pack_id): """ + Description: Query package layer 1 installation dependency :param dbname: databases name :param src_pack_id: srcpackage id :return: subpack Source package to binary package, then find the installation dependencies @@ -92,6 +95,8 @@ def sub_packages(dbname, src_pack_id): def get_single_package(dbname, sourcename): ''' Description: Get all packages info + :param dbname: Database name + :param sourcename: Source package name :return: package info ''' with DBHelper(db_name=dbname) as db_name: @@ -120,7 +125,11 @@ def update_single_package( maintainer, maintain_level): """ - change single package management + Description: change single package management + :param package_name: package name + :param dbname: Database name + :param maintainer: maintainer info + :param maintain_level: maintain_level info :return: message success or failed """ with DBHelper(db_name=dbname) as db_name: @@ -136,7 +145,14 @@ def update_maintaniner_info(package_name, maintaniner, maintainlevel): ''' - Separate maintenance information + """ + update separately maintaniner info + :param package_name: package name + :param dbname: Database name + :param maintainer: maintainer info + :param maintain_level: maintain_level info + :return: message success or failed + """ ''' with DBHelper(db_name=dbname) as db_name: src_pack_obj = db_name.session.query(src_pack).filter_by( diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index b66cb916..a67c41c8 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -450,7 +450,7 @@ class InitSystem(Resource): except FileNotFoundError as file_not_found_error: LOGGER.logger.error(file_not_found_error) abnormal = ResponseCode.FILE_NOT_FIND_ERROR - except (Error, Exception) as error: + except Error as error: LOGGER.logger.error(error) abnormal = ResponseCode.FAILED_CREATE_DATABASE_TABLE if abnormal is not None: -- Gitee From 14a4da775a00c9c4460639075fa4c48e8fcef0e7 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Sun, 28 Jun 2020 17:17:17 +0800 Subject: [PATCH 23/72] add querying install depend function --- .../apps/package/function/install_depend.py | 133 ++++++++++++++++++ .../apps/package/function/searchdb.py | 119 ++++++++++++++++ .../application/apps/package/view.py | 101 +++++++++---- packageship/packageship/system_config.py | 2 +- 4 files changed, 328 insertions(+), 27 deletions(-) create mode 100644 packageship/packageship/application/apps/package/function/install_depend.py diff --git a/packageship/packageship/application/apps/package/function/install_depend.py b/packageship/packageship/application/apps/package/function/install_depend.py new file mode 100644 index 00000000..212ab573 --- /dev/null +++ b/packageship/packageship/application/apps/package/function/install_depend.py @@ -0,0 +1,133 @@ +''' + Querying for install dependencies + Querying packages install depend for those package can be installed +''' +from packageship.libs.log import Log +from .searchdb import SearchDB +from .constants import ResponseCode +from .constants import ListNode + +LOGGER = Log(__name__) + + +class InstallDepend(): + ''' + Description: quert install depend of package + changeLog: + ''' + + def __init__(self, db_list): + ''' + :param db_list: A list of Database name to show the priority + ''' + self.binary_dict = DictionaryOperations() + self.search_list = [] + + self.db_list = db_list + self.search_db = SearchDB(db_list) + + def install_depend_result(self, binary_list, history_dicts=None): + ''' + Description: init result dict and determint the loop end point + :param binary_list: A list of binary rpm package name + history_dicts: record the searching install depend history, + defualt is None + :return binary_dict.dictionary: + {binary_name: [ + src, + dbname, + version, + [ + parent_node_package_name + 'install' + ] + ]} + ''' + if not self.search_db.db_object_dict: + return ResponseCode.DIS_CONNECTION_DB, None + if not binary_list: + response_code = ResponseCode.INPUT_NONE, + return response_code, None + for binary in binary_list: + if binary: + self.search_list.append(binary) + else: + LOGGER.logger.warning("There is a NONE in input value:" + str(binary_list)) + while self.search_list: + response_code = self.query_install(history_dicts) + return response_code, self.binary_dict.dictionary if response_code != \ + ResponseCode.DIS_CONNECTION_DB else None + + def query_install(self, history_dicts): + """ + Description: query a package install depend and append to result + :param history_dicts + :return response_code + changeLog: + """ + response_code, result_list = self.search_db.get_install_depend(self.search_list) + for search in self.search_list: + if search not in self.binary_dict.dictionary: + self.binary_dict.init_key(key=search, parent_node=[]) + self.search_list.clear() + if result_list: + for result, dbname in result_list: + if not self.binary_dict.dictionary[result.search_name][ListNode.PARENT_LIST]: + self.binary_dict.init_key(key=result.search_name, + src=result.search_src_name, + version=result.search_version, + dbname=dbname) + else: + self.binary_dict.update_value(key=result.search_name, + src=result.search_src_name, + version=result.search_version, + dbname=dbname) + + if result.depend_name: + if result.depend_name in self.binary_dict.dictionary: + self.binary_dict.update_value(key=result.depend_name, + parent_node=[result.search_name, 'install']) + elif history_dicts is not None and result.depend_name in history_dicts: + self.binary_dict.init_key( + key=result.depend_name, + src=history_dicts[result.depend_name][ListNode.SOURCE_NAME], + version=history_dicts[result.depend_name][ListNode.VERSION], + dbname=None, + parent_node=[[result.search_name, 'install']] + ) + else: + self.binary_dict.init_key(key=result.depend_name, + parent_node=[[result.search_name, 'install']]) + self.search_list.append(result.depend_name) + return response_code + +class DictionaryOperations(): + ''' + Related to dictionary operations, creating dictionary, append dictionary + ''' + + def __init__(self): + + self.dictionary = dict() + + def init_key(self, key, src=None, version=None, dbname=None, parent_node=None): + ''' + Creating dictionary + ''' + if dbname: + self.dictionary[key] = [src, version, dbname, [['root', None]]] + else: + self.dictionary[key] = [src, version, dbname, parent_node] + + def update_value(self, key, src=None, version=None, dbname=None, parent_node=None): + ''' + append dictionary + ''' + if src: + self.dictionary[key][ListNode.SOURCE_NAME] = src + if version: + self.dictionary[key][ListNode.VERSION] = version + if dbname: + self.dictionary[key][ListNode.DBNAME] = dbname + if parent_node: + self.dictionary[key][ListNode.PARENT_LIST].append(parent_node) diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py index 374456f8..0128db13 100644 --- a/packageship/packageship/application/apps/package/function/searchdb.py +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -1,13 +1,132 @@ """ A set for all query databases function """ +from collections import namedtuple import yaml from flask import current_app +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError, DisconnectionError +from sqlalchemy.sql import literal_column +from packageship.libs.dbutils import DBHelper +from packageship.libs.log import Log +from packageship.application.models.package import bin_pack from packageship.libs.exception import ContentNoneException, Error from packageship.system_config import DATABASE_SUCCESS_FILE +from .constants import ResponseCode +LOGGER = Log(__name__) + +class SearchDB(): + """ + Description: query in database + changeLog: + """ + def __new__(cls, *args, **kwargs): + # pylint: disable=w0613 + if not hasattr(cls, "_instance"): + cls._instance = super(SearchDB, cls).__new__(cls) + return cls._instance + + def __init__(self, db_list): + self.db_object_dict = dict() + for db_name in db_list: + try: + with DBHelper(db_name=db_name) as data_base: + self.db_object_dict[db_name] = data_base + except DisconnectionError as connection_error: + current_app.logger.error(connection_error) + + def get_install_depend(self, binary_list): + """ + Description: get a package install depend from database: + binary_name -> binary_id -> requires_set -> requires_id_set -> provides_set + -> install_depend_binary_id_key_list -> install_depend_binary_name_list + :param binary_lsit: a list of binary package name + :return install depend list + changeLog: + """ + if not self.db_object_dict: + return ResponseCode.DIS_CONNECTION_DB, None + + if None in binary_list: + binary_list.remove(None) + search_set = set(binary_list) + result_list = [] + get_list = [] + if not search_set: + return ResponseCode.INPUT_NONE, None + for db_name, data_base in self.db_object_dict.items(): + try: + name_in = literal_column('name').in_(search_set) + sql_com = text(""" + SELECT DISTINCT + bin_pack.NAME AS depend_name, + bin_pack.version AS depend_version, + s2.NAME AS depend_src_name, + bin.NAME AS search_name, + s1.`name` AS search_src_name, + s1.version AS search_version + FROM + ( SELECT id, NAME,srcIDkey FROM bin_pack WHERE {} ) bin + LEFT JOIN pack_requires ON bin.id = pack_requires.binIDkey + LEFT JOIN pack_provides ON pack_provides.id = pack_requires.depProIDkey + LEFT JOIN bin_pack ON bin_pack.id = pack_provides.binIDkey + LEFT JOIN src_pack s1 ON s1.id = bin.srcIDkey + LEFT JOIN src_pack s2 ON s2.id = bin_pack.srcIDkey;""".format(name_in)) + install_set = data_base.session. \ + execute(sql_com, {'name_{}'.format(i): v + for i, v in enumerate(search_set, 1)}).fetchall() + if install_set: + # find search_name in db_name + # depend_name's db_name will be found in next loop + for result in install_set: + result_list.append((result, db_name)) + get_list.append(result.search_name) + get_set = set(get_list) + get_list.clear() + search_set.symmetric_difference_update(get_set) + if not search_set: + return ResponseCode.SUCCESS, result_list + else: + continue + except AttributeError as error_msg: + LOGGER.logger.error(error_msg) + except SQLAlchemyError as error_msg: + LOGGER.logger.error(error_msg) + return_tuple = namedtuple('return_tuple', + 'depend_name depend_version depend_src_name \ + search_name search_src_name search_version') + for binary_name in search_set: + result_list.append((return_tuple(None, None, None, + binary_name, None, None), 'NOT FOUND')) + return ResponseCode.SUCCESS, result_list + + def get_src_name(self, binary_name): + """ + Description: get a package source name from database: + bianry_name ->binary_source_name -> source_name + input: search package's name, database preority list + return: database name, source name + changeLog: + """ + for db_name, data_base in self.db_object_dict.items(): + try: + bin_obj = data_base.session.query(bin_pack).filter_by( + name=binary_name + ).first() + source_name = bin_obj.src_pack.name + source_version = bin_obj.src_pack.version + if source_name is not None: + return ResponseCode.SUCCESS, db_name, \ + source_name, source_version + except AttributeError as error_msg: + LOGGER.logger.error(error_msg) + except SQLAlchemyError as error_msg: + LOGGER.logger.error(error_msg) + return ResponseCode.DIS_CONNECTION_DB, None + return ResponseCode.PACK_NAME_NOT_FOUND, None, None, None def db_priority(): """ diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index a67c41c8..795427b2 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -8,17 +8,6 @@ from flask import current_app from flask_restful import Resource from sqlalchemy.exc import DisconnectionError -from packageship.application.apps.package.function.constants import ResponseCode -from packageship.application.apps.package.function.packages import get_packages -from packageship.application.apps.package.function.packages import update_single_package -from packageship.application.apps.package.function.packages import update_maintaniner_info -from packageship.application.apps.package.function.packages import get_single_package -from packageship.application.apps.package.function.searchdb import db_priority -from packageship.application.apps.package.serialize import PackagesSchema -from packageship.application.apps.package.serialize import GetpackSchema -from packageship.application.apps.package.serialize import PutpackSchema -from packageship.application.apps.package.serialize import DeletedbSchema -from packageship.application.apps.package.serialize import InitSystemSchema from packageship.application.initsystem.data_import import InitDataBase from packageship.libs.configutils.readconfig import ReadConfig from packageship.libs.exception import Error @@ -26,9 +15,24 @@ from packageship.libs.exception import ContentNoneException from packageship.libs.exception import DataMergeException from packageship.libs.log import Log from packageship.system_config import DATABASE_SUCCESS_FILE +from .function.constants import ResponseCode +from .function.packages import get_packages +from .function.packages import update_single_package +from .function.packages import update_maintaniner_info +from .function.packages import get_single_package +from .function.searchdb import db_priority +from .serialize import PackagesSchema +from .serialize import GetpackSchema +from .serialize import PutpackSchema +from .serialize import DeletedbSchema +from .serialize import InitSystemSchema + +from .function.install_depend import InstallDepend as installdepend +from .serialize import InstallDependSchema +from .serialize import have_err_db_name LOGGER = Log(__name__) - +#pylint: disable = no-self-use class Packages(Resource): ''' @@ -212,7 +216,7 @@ class InstallDepend(Resource): changeLog: ''' - def post(self, *args, **kwargs): + def post(self): ''' Description: Query a package's install depend(support querying in one or more databases) @@ -220,22 +224,66 @@ class InstallDepend(Resource): binaryName dbPreority:the array for database preority return: - resultList[ - result[ - binaryName: binary package name - srcName: the source package name for - that binary packge - dbName: - type: install install or build, which - depend on the function - parentNode: the binary package name which is - the install depend for binaryName + resultDict{ + binary_name: //binary package name + [ + src, //the source package name for + that binary packge + dbname, + version, + [ + parent_node, //the binary package name which is + the install depend for binaryName + type //install install or build, which + depend on the function + ] ] - ] + } exception: changeLog: ''' - pass + schema = InstallDependSchema() + + data = request.get_json() + validate_err = schema.validate(data) + if validate_err: + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + pkg_name = data.get("binaryName") + + db_pri = db_priority() + if not db_pri: + return jsonify( + ResponseCode.response_json( + ResponseCode.FILE_NOT_FIND_ERROR + ) + ) + + db_list = data.get("db_list") if data.get("db_list") \ + else db_pri + + if not all([pkg_name, db_list]): + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + + if have_err_db_name(db_list, db_pri): + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + + response_code, install_dict = \ + installdepend(db_list).install_depend_result([pkg_name]) + + if not install_dict: + return jsonify( + ResponseCode.response_json(response_code) + ) + + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS, data=install_dict) + ) class BuildDepend(Resource): @@ -330,7 +378,8 @@ class BeDepend(Resource): srcName: dbName: type: beinstall or bebuild, which depend on the function - childNode: the binary package name which is the be built/installed depend for binaryName + childNode: the binary package name which is the be built/installed + depend for binaryName ] ] exception: diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py index b26d7598..8db61286 100644 --- a/packageship/packageship/system_config.py +++ b/packageship/packageship/system_config.py @@ -23,7 +23,7 @@ SYS_CONFIG_PATH = os.path.join('/', 'etc', 'pkgship', 'package.ini') # BASE_PATH, 'application', 'initsystem', 'import_success_databse.yaml') DATABASE_SUCCESS_FILE = os.path.join( - '/', 'var', 'run', 'import_success_databse.yaml') + '/', 'var', 'run', 'import_success_database.yaml') # If the path of the imported database is not specified in the configuration file, the # configuration in the system is used by default -- Gitee From e134f61a9c1859238f6b9beb9ea1161afb4aa4a2 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Sun, 28 Jun 2020 17:26:22 +0800 Subject: [PATCH 24/72] add querying bedepend function --- .../apps/package/function/be_depend.py | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 packageship/packageship/application/apps/package/function/be_depend.py diff --git a/packageship/packageship/application/apps/package/function/be_depend.py b/packageship/packageship/application/apps/package/function/be_depend.py new file mode 100644 index 00000000..8740d8b9 --- /dev/null +++ b/packageship/packageship/application/apps/package/function/be_depend.py @@ -0,0 +1,198 @@ +# -*- coding:utf-8 -*- +''' + The dependencies of the query package + Used for package deletion and upgrade scenarios + This includes both install and build dependencies +''' +from sqlalchemy import text +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql import literal_column +from flask import current_app +from packageship.libs.dbutils import DBHelper +from packageship.application.models.package import src_pack +from packageship.application.apps.package.function.constants import ResponseCode + +class BeDepend: + ''' + Find the dependencies of the source package + ''' + + def __init__(self, source_name, db_name, with_sub_pack): + ''' + :param source_name: source_name + :param db_name: db_name + :param with_sub_pack: with_sub_pack + ''' + self.source_name = source_name + self.db_name = db_name + self.with_sub_pack = with_sub_pack + # Source package lookup set + self.source_name_set = set() + # Bin package lookup set + self.bin_name_set = set() + # return json + self.result_dict = dict() + + def main(self): + ''' + Map the database, if the source package of the query is not in the database, + throw an exception. Directly to the end + :return: + ''' + with DBHelper(db_name=self.db_name) as data_base: + src_obj = data_base.session.query( + src_pack).filter_by(name=self.source_name).first() + if src_obj: + # 拼字典 + self.result_dict[self.source_name + "_src"] = [ + "source", + src_obj.version, + self.db_name, + [["root", None]] + ] + self.source_name_set.add(self.source_name) + self.package_bedepend( + [src_obj.id], data_base, package_type='src') + + return self.result_dict + + def package_bedepend(self, pkg_id_list, data_base, package_type): + ''' + Query the dependent function + :param pkg_id_list:source or binary packages id + :param data_base: database + :param package_type: package type + :return: + ''' + # package_type + search_set = set(pkg_id_list) + id_in = literal_column('id').in_(search_set) + # package_type + if package_type == 'src': + sql_str = text(""" + SELECT b1.name AS search_bin_name, + b1.version AS search_bin_version, + src.NAME AS source_name, + b2.name AS bin_name, + b2.id AS bin_id, + s1.name AS src_name, + s1.id AS src_id, + s2.name AS install_depend_src_name, + s2.id AS install_depend_src_id + FROM + ( SELECT id,NAME FROM src_pack WHERE {} ) src + LEFT JOIN bin_pack b1 ON b1.srcIDkey = src.id + LEFT JOIN pack_provides ON pack_provides.binIDkey = b1.id + LEFT JOIN pack_requires ON pack_requires.depProIDkey = pack_provides.id + LEFT JOIN src_pack s1 ON s1.id = pack_requires.srcIDkey + LEFT JOIN bin_pack b2 ON b2.id = pack_requires.binIDkey + LEFT JOIN src_pack s2 ON s2.id = b2.srcIDkey;""".format(id_in)) + if package_type == 'bin': + sql_str = text(""" + SELECT b1.name AS search_bin_name, + b1.version AS search_bin_version, + s3.NAME AS source_name, + b2.name AS bin_name, + b2.id AS bin_id, + s1.name AS src_name, + s1.id AS src_id, + s2.name AS install_depend_src_name, + s2.id AS install_depend_src_id + FROM + (SELECT id,NAME,version,srcIDkey FROM bin_pack WHERE {} ) b1 + LEFT JOIN src_pack s3 ON s3.id = b1.srcIDkey + LEFT JOIN pack_provides ON pack_provides.binIDkey = b1.id + LEFT JOIN pack_requires ON pack_requires.depProIDkey = pack_provides.id + LEFT JOIN src_pack s1 ON s1.id = pack_requires.srcIDkey + LEFT JOIN bin_pack b2 ON b2.id = pack_requires.binIDkey + LEFT JOIN src_pack s2 ON s2.id = b2.srcIDkey; + """.format(id_in)) + try: + result = data_base.session.execute( + sql_str, { + 'id_{}'.format(i): v for i, v in enumerate( + search_set, 1)}).fetchall() + except SQLAlchemyError as sql_err: + current_app.logger.error(sql_err) + return ResponseCode.CONNECT_DB_ERROR, None + + if result is None: + return + # Source and binary packages that were found to be dependent + source_id_list = [] + bin_id_list = [] + for obj in result: + if obj.source_name is None: + source_name = 'NOT FOUND' + else: + source_name = obj.source_name + if obj.src_name: + # Determine if the source package has been checked + if obj.src_name not in self.source_name_set: + + self.source_name_set.add(obj.src_name) + source_id_list.append(obj.src_id) + + parent_node = obj.src_name + be_type = "build" + # Call the spell dictionary function + self.make_dicts( + obj.search_bin_name, + source_name, + obj.search_bin_version, + parent_node, + be_type) + + if obj.bin_name: + # Determine if the bin package has been checked + if obj.bin_name not in self.bin_name_set: + self.bin_name_set.add(obj.bin_name) + bin_id_list.append(obj.bin_id) + + parent_node = obj.bin_name + be_type = "install" + # Call the spell dictionary function + self.make_dicts( + obj.search_bin_name, + source_name, + obj.search_bin_version, + parent_node, + be_type) + # withsubpack=1 + if self.with_sub_pack == "1": + if obj.install_depend_src_name not in self.source_name_set: + self.source_name_set.add( + obj.install_depend_src_name) + source_id_list.append(obj.install_depend_src_id) + + if len(source_id_list) != 0: + self.package_bedepend( + source_id_list, data_base, package_type="src") + if len(bin_id_list) != 0: + self.package_bedepend(bin_id_list, data_base, package_type="bin") + + def make_dicts(self, key, source_name, version, parent_node, be_type): + ''' + :param key: dependent bin name + :param source_name: source name + :param version: version + :param parent_node: Rely on package name + :param be_type: dependent type + :return: + ''' + if key not in self.result_dict: + self.result_dict[key] = [ + source_name, + version, + self.db_name, + [ + [parent_node, + be_type + ] + ] + ] + else: + self.result_dict[key][-1].append([ + parent_node, + be_type + ]) -- Gitee From 799aedec51277e40427fef9f82be0799ed827771 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Sun, 28 Jun 2020 23:28:54 +0800 Subject: [PATCH 25/72] change the public method to private and change the method name --- .../apps/package/function/constants.py | 1 + .../apps/package/function/install_depend.py | 15 ++++++--------- .../apps/package/function/searchdb.py | 16 +++++++++------- .../packageship/application/apps/package/view.py | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/constants.py b/packageship/packageship/application/apps/package/function/constants.py index 09a7bfa3..241855f0 100644 --- a/packageship/packageship/application/apps/package/function/constants.py +++ b/packageship/packageship/application/apps/package/function/constants.py @@ -53,6 +53,7 @@ class ResponseCode(): PACK_NAME_NOT_FOUND: "Sorry! The querying package does not exist in the databases", CONNECT_DB_ERROR: "Failed to Connect the database! " "Please check the database connection", + INPUT_NONE: "The input is None, please check the input value.", FILE_NOT_FOUND: "Database import success file does not exist", DELETE_DB_ERROR: "Failed to delete database", CONFIGFILE_PATH_EMPTY: "Initialization profile does not exist or cannot be found", diff --git a/packageship/packageship/application/apps/package/function/install_depend.py b/packageship/packageship/application/apps/package/function/install_depend.py index 212ab573..cc6ec382 100644 --- a/packageship/packageship/application/apps/package/function/install_depend.py +++ b/packageship/packageship/application/apps/package/function/install_depend.py @@ -26,7 +26,7 @@ class InstallDepend(): self.db_list = db_list self.search_db = SearchDB(db_list) - def install_depend_result(self, binary_list, history_dicts=None): + def query_install_depend(self, binary_list, history_dicts=None): ''' Description: init result dict and determint the loop end point :param binary_list: A list of binary rpm package name @@ -46,26 +46,24 @@ class InstallDepend(): if not self.search_db.db_object_dict: return ResponseCode.DIS_CONNECTION_DB, None if not binary_list: - response_code = ResponseCode.INPUT_NONE, - return response_code, None + return ResponseCode.INPUT_NONE, None for binary in binary_list: if binary: self.search_list.append(binary) else: LOGGER.logger.warning("There is a NONE in input value:" + str(binary_list)) while self.search_list: - response_code = self.query_install(history_dicts) - return response_code, self.binary_dict.dictionary if response_code != \ - ResponseCode.DIS_CONNECTION_DB else None + self.__query_single_install_dep(history_dicts) + return ResponseCode.SUCCESS, self.binary_dict.dictionary - def query_install(self, history_dicts): + def __query_single_install_dep(self, history_dicts): """ Description: query a package install depend and append to result :param history_dicts :return response_code changeLog: """ - response_code, result_list = self.search_db.get_install_depend(self.search_list) + result_list = self.search_db.get_install_depend(self.search_list) for search in self.search_list: if search not in self.binary_dict.dictionary: self.binary_dict.init_key(key=search, parent_node=[]) @@ -99,7 +97,6 @@ class InstallDepend(): self.binary_dict.init_key(key=result.depend_name, parent_node=[[result.search_name, 'install']]) self.search_list.append(result.depend_name) - return response_code class DictionaryOperations(): ''' diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py index 0128db13..4eaebe7a 100644 --- a/packageship/packageship/application/apps/package/function/searchdb.py +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -47,16 +47,18 @@ class SearchDB(): :return install depend list changeLog: """ + result_list = [] + get_list = [] if not self.db_object_dict: - return ResponseCode.DIS_CONNECTION_DB, None - + LOGGER.logger.warning("Unable to connect to the database, \ + check the database configuration") + return result_list if None in binary_list: binary_list.remove(None) search_set = set(binary_list) - result_list = [] - get_list = [] if not search_set: - return ResponseCode.INPUT_NONE, None + LOGGER.logger.warning("The input is None, please check the input value.") + return result_list for db_name, data_base in self.db_object_dict.items(): try: name_in = literal_column('name').in_(search_set) @@ -88,7 +90,7 @@ class SearchDB(): get_list.clear() search_set.symmetric_difference_update(get_set) if not search_set: - return ResponseCode.SUCCESS, result_list + return result_list else: continue except AttributeError as error_msg: @@ -101,7 +103,7 @@ class SearchDB(): for binary_name in search_set: result_list.append((return_tuple(None, None, None, binary_name, None, None), 'NOT FOUND')) - return ResponseCode.SUCCESS, result_list + return result_list def get_src_name(self, binary_name): """ diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 795427b2..8f78f271 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -274,7 +274,7 @@ class InstallDepend(Resource): ) response_code, install_dict = \ - installdepend(db_list).install_depend_result([pkg_name]) + installdepend(db_list).query_install_depend([pkg_name]) if not install_dict: return jsonify( -- Gitee From 7c049a5fdc7d01bd660f19cf05416323ac1106e3 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 29 Jun 2020 09:21:48 +0800 Subject: [PATCH 26/72] change object to private --- .../apps/package/function/install_depend.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/install_depend.py b/packageship/packageship/application/apps/package/function/install_depend.py index cc6ec382..6d9bf57f 100644 --- a/packageship/packageship/application/apps/package/function/install_depend.py +++ b/packageship/packageship/application/apps/package/function/install_depend.py @@ -15,16 +15,16 @@ class InstallDepend(): Description: quert install depend of package changeLog: ''' - + #pylint: disable = too-few-public-methods def __init__(self, db_list): ''' :param db_list: A list of Database name to show the priority ''' self.binary_dict = DictionaryOperations() - self.search_list = [] + self.__search_list = [] self.db_list = db_list - self.search_db = SearchDB(db_list) + self.__search_db = SearchDB(db_list) def query_install_depend(self, binary_list, history_dicts=None): ''' @@ -43,16 +43,16 @@ class InstallDepend(): ] ]} ''' - if not self.search_db.db_object_dict: + if not self.__search_db.db_object_dict: return ResponseCode.DIS_CONNECTION_DB, None if not binary_list: return ResponseCode.INPUT_NONE, None for binary in binary_list: if binary: - self.search_list.append(binary) + self.__search_list.append(binary) else: LOGGER.logger.warning("There is a NONE in input value:" + str(binary_list)) - while self.search_list: + while self.__search_list: self.__query_single_install_dep(history_dicts) return ResponseCode.SUCCESS, self.binary_dict.dictionary @@ -63,11 +63,11 @@ class InstallDepend(): :return response_code changeLog: """ - result_list = self.search_db.get_install_depend(self.search_list) - for search in self.search_list: + result_list = self.__search_db.get_install_depend(self.__search_list) + for search in self.__search_list: if search not in self.binary_dict.dictionary: self.binary_dict.init_key(key=search, parent_node=[]) - self.search_list.clear() + self.__search_list.clear() if result_list: for result, dbname in result_list: if not self.binary_dict.dictionary[result.search_name][ListNode.PARENT_LIST]: @@ -96,7 +96,7 @@ class InstallDepend(): else: self.binary_dict.init_key(key=result.depend_name, parent_node=[[result.search_name, 'install']]) - self.search_list.append(result.depend_name) + self.__search_list.append(result.depend_name) class DictionaryOperations(): ''' -- Gitee From 6083b71db95b225ee93ea80af3d866fd750569c4 Mon Sep 17 00:00:00 2001 From: gongzt Date: Mon, 29 Jun 2020 15:17:14 +0800 Subject: [PATCH 27/72] Wrong judgment of configuration file type, change of file name of generated database --- packageship/.gitignore | 4 +++- .../packageship/application/initsystem/data_import.py | 8 +++++--- packageship/packageship/system_config.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packageship/.gitignore b/packageship/.gitignore index 04ed6add..2027f52c 100644 --- a/packageship/.gitignore +++ b/packageship/.gitignore @@ -1,2 +1,4 @@ .DS_Store -*/.DS_Store \ No newline at end of file +*/.DS_Store +*.pyc +*.vscode \ No newline at end of file diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index eebd0969..2c6a7ff1 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -74,7 +74,9 @@ class InitDataBase(): 'The content of the database initialization configuration file cannot be empty') if not isinstance(init_database_config, list): raise TypeError('wrong type of configuration file') - + for config_item in init_database_config: + if not isinstance(config_item, dict): + raise TypeError('wrong type of configuration file') return init_database_config def init_data(self): @@ -101,14 +103,14 @@ class InitDataBase(): 'maintenance.information').create_datum_database() for database in self.config_file_datas: - if not isinstance(database, dict): - continue if not database.get('dbname'): continue priority = database.get('priority') if not isinstance(priority, int) or priority < 0 or priority > 100: continue status = database.get('status') + if status not in ['enable', 'disable']: + continue # Initialization data self._init_data(database) diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py index b26d7598..26c0dffc 100644 --- a/packageship/packageship/system_config.py +++ b/packageship/packageship/system_config.py @@ -23,7 +23,7 @@ SYS_CONFIG_PATH = os.path.join('/', 'etc', 'pkgship', 'package.ini') # BASE_PATH, 'application', 'initsystem', 'import_success_databse.yaml') DATABASE_SUCCESS_FILE = os.path.join( - '/', 'var', 'run', 'import_success_databse.yaml') + '/', 'var', 'run', 'database_file_info.yaml') # If the path of the imported database is not specified in the configuration file, the # configuration in the system is used by default -- Gitee From 6c4d4191a3de5bfa3ea7c72f5181879293c496c8 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Mon, 29 Jun 2020 16:39:49 +0800 Subject: [PATCH 28/72] add test dir --- packageship/test/test_install_depend.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packageship/test/test_install_depend.py diff --git a/packageship/test/test_install_depend.py b/packageship/test/test_install_depend.py new file mode 100644 index 00000000..e69de29b -- Gitee From 7b4a66e358a4367132ebac1f9e987959739b6dc3 Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Mon, 29 Jun 2020 17:01:15 +0800 Subject: [PATCH 29/72] add build_depend.py file,and modify view.py BuildDepend class in file,add two functions in searchdb.py,modify sqlite params in sqlalchemy_helper.py. --- .../apps/package/function/build_depend.py | 191 ++++++++++++++++++ .../apps/package/function/searchdb.py | 184 ++++++++++++++++- .../application/apps/package/view.py | 41 +++- .../libs/dbutils/sqlalchemy_helper.py | 3 +- 4 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 packageship/packageship/application/apps/package/function/build_depend.py diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py new file mode 100644 index 00000000..c4efe919 --- /dev/null +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -0,0 +1,191 @@ +""" +Find compilation dependency of source package +""" +from packageship.application.apps.package.function.searchdb import SearchDB +from packageship.application.apps.package.function.install_depend import InstallDepend +from packageship.application.apps.package.function.constants import ResponseCode, ListNode + + + +class BuildDepend: + """ + Find compilation dependency of source package + """ + + def __init__(self, pkg_name_list, db_list, self_build=0, history_dict=None): + self.pkg_name_list = pkg_name_list + self._self_build = self_build + + self.db_list = db_list + self.search_db = SearchDB(db_list) + + self.result_dict = dict() + self.source_dict = dict() + + self.history_dicts = history_dict if history_dict else {} + + def build_depend_main(self): + """ + Entry function + :return: ResponseCode,result_dict,source_dict + """ + if not self.search_db.db_object_dict: + return ResponseCode.DIS_CONNECTION_DB, None, None + + if self._self_build == 0: + code = self.build_depend(self.pkg_name_list) + if None in self.result_dict: + del self.result_dict[None] + return code, self.result_dict, None + + if self._self_build == 1: + self.self_build(self.pkg_name_list) + if None in self.result_dict: + del self.result_dict[None] + return ResponseCode.SUCCESS, self.result_dict, self.source_dict + + return ResponseCode.PARAM_ERROR, None, None + + def build_depend(self, pkg_list): + """ + @:param pkg_list:You need to find the dependent source package name + :return ResponseCode + """ + res_status, build_list = self.search_db.get_build_depend(pkg_list) + + if not build_list: + return res_status if res_status == \ + ResponseCode.DIS_CONNECTION_DB else \ + ResponseCode.PACK_NAME_NOT_FOUND + # create root node and get next search list + search_list = self._create_node_and_get_search_list(build_list, pkg_list) + + code, res_dict = \ + InstallDepend(self.db_list).query_install_depend(search_list, + self.history_dicts) + if not res_dict: + return code + + for k, values in res_dict.items(): + if k in self.result_dict: + if ['root', None] in values[ListNode.PARENT_LIST]: + index = values[ListNode.PARENT_LIST].index(['root', None]) + del values[ListNode.PARENT_LIST][index] + + self.result_dict[k][ListNode.PARENT_LIST].extend(values[ListNode.PARENT_LIST]) + else: + self.result_dict[k] = values + + return ResponseCode.SUCCESS + + def _create_node_and_get_search_list(self, build_list, pkg_list): + """ + To create root node in self.result_dict and + return the name of the source package to be found next time + @:param build_list:List of binary package names + @:param pkg_list: List of binary package names + :return the name of the source package to be found next time + """ + search_set = set() + search_list = [] + for obj in build_list: + if not obj.search_name: + continue + + if obj.search_name + "_src" not in self.result_dict: + self.result_dict[obj.search_name + "_src"] = [ + 'source', + obj.search_version, + obj.db_name, + [ + ['root', None] + ] + ] + search_set.add(obj.search_name) + + if not obj.bin_name: + continue + + if obj.bin_name in self.history_dicts: + self.result_dict[obj.bin_name] = [ + self.history_dicts[obj.bin_name][ListNode.SOURCE_NAME], + self.history_dicts[obj.bin_name][ListNode.VERSION], + self.history_dicts[obj.bin_name][ListNode.DBNAME], + [ + [obj.search_name, 'build'] + ] + ] + else: + if obj.bin_name in search_list: + self.result_dict[obj.bin_name][ListNode.PARENT_LIST].append([ + obj.search_name, 'build' + ]) + else: + self.result_dict[obj.bin_name] = [ + obj.source_name, + obj.version, + obj.db_name, + [ + [obj.search_name, 'build'] + ] + ] + search_list.append(obj.bin_name) + + if search_set and len(search_set) != len(pkg_list): + temp_set = set(pkg_list) - search_set + for name in temp_set: + self.result_dict[name + "_src"] = [ + None, + None, + 'NOT_FOUND', + [ + ['root', None] + ] + ] + return search_list + + def self_build(self, pkg_name_li): + """ + Using recursion to find compilation dependencies + :param pkg_name_li: Source package name list + :return: + """ + if not pkg_name_li: + return None + + next_src_set = set() + _, bin_info_lis = self.search_db.get_build_depend(pkg_name_li) + + if not bin_info_lis: + return None + + # generate data content + for obj in bin_info_lis: + + if not obj.bin_name: + continue + + if obj.bin_name not in self.result_dict: + self.result_dict[obj.bin_name] = [ + obj.source_name if obj.source_name else None, + obj.version if obj.version else None, + obj.db_name if obj.db_name else "NOT_FOUND", + [ + [obj.search_name, "build"] + ] + ] + else: + node = [obj.search_name, "build"] + node_list = self.result_dict[obj.bin_name][-1] + if node not in node_list: + node_list.append(node) + + if obj.source_name and \ + obj.source_name not in self.source_dict: + self.source_dict[obj.source_name] = [obj.db_name, + obj.version] + next_src_set.add(obj.source_name) + + self.self_build(next_src_set) + + return None diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py index 4eaebe7a..81702e65 100644 --- a/packageship/packageship/application/apps/package/function/searchdb.py +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -122,7 +122,7 @@ class SearchDB(): source_version = bin_obj.src_pack.version if source_name is not None: return ResponseCode.SUCCESS, db_name, \ - source_name, source_version + source_name, source_version except AttributeError as error_msg: LOGGER.logger.error(error_msg) except SQLAlchemyError as error_msg: @@ -130,6 +130,188 @@ class SearchDB(): return ResponseCode.DIS_CONNECTION_DB, None return ResponseCode.PACK_NAME_NOT_FOUND, None, None, None + def get_binary_in_other_database(self, not_found_binary, db_): + """ + Binary package name data not found in the current database, go to other databases to try + @:param:not_found_build These data cannot be found in the current database + @:param:db:current database name + return:a list :[(search_name,source_name,bin_name, + bin_version,db_name,search_version,req_name), + (search_name,source_name,bin_name, + bin_version,db_name,search_version,req_name),] + changeLog:new method to look for data in other databases + """ + if not not_found_binary: + return [] + + return_tuple = namedtuple("return_tuple", [ + "search_name", + "source_name", + "bin_name", + "version", + "db_name", + "search_version", + "req_name" + ]) + src_req_map = {req_: src for src, req_ in not_found_binary} + + local_search_set = {req_ for _, req_ in not_found_binary} + + local_dict = {k: v for k, v in self.db_object_dict.items() if k != db_} + res = [] + + for db_name, data_base in local_dict.items(): + try: + sql_string = text(""" + SELECT + t3.NAME AS source_name, + t1.NAME AS bin_name, + t1.version, + t3.version as search_version, + t2.NAME AS req_name + FROM + bin_pack t1, + pack_provides t2, + src_pack t3 + WHERE + t2.{} + AND t1.id = t2.binIDkey + AND t1.srcIDkey = t3.id; + """.format(literal_column('name').in_(local_search_set))) + build_set_2 = data_base.session. \ + execute(sql_string, {'name_{}'.format(i): v + for i, v in enumerate(local_search_set, 1)}).fetchall() + if not build_set_2: + continue + + res.extend([return_tuple( + src_req_map.get(bin_pack.req_name), + bin_pack.source_name, + bin_pack.bin_name, + bin_pack.version, + db_name, + bin_pack.search_version, + bin_pack.req_name + ) for bin_pack in build_set_2 if bin_pack.bin_name]) + + for obj in res: + local_search_set.remove(obj.req_name) + + except AttributeError as attr_error: + current_app.logger.error(attr_error) + except SQLAlchemyError as sql_error: + current_app.logger.error(sql_error) + return res + + def get_build_depend(self, source_name_li): + """ + Description: get a package build depend from database + input: + @:param: search package's name list + return: all source pkg build depend list + structure :[(search_name,source_name,bin_name,bin_version,db_name,search_version), + (search_name,source_name,bin_name,bin_version,db_name,search_version),] + + changeLog: Modify SQL logic and modify return content by:zhangtao + """ + # pylint: disable=R0914 + return_tuple = namedtuple("return_tuple", [ + "search_name", + "source_name", + "bin_name", + "version", + "db_name", + "search_version" + ]) + + s_name_set = set(source_name_li) + if not s_name_set: + return ResponseCode.PARAM_ERROR, None + + not_found_binary = set() + build_list = [] + + for db_name, data_base in self.db_object_dict.items(): + try: + sql_com = text("""SELECT DISTINCT + src.NAME AS search_name, + src.version AS search_version, + s2.NAME AS source_name, + pack_provides.binIDkey AS bin_id, + pack_requires.NAME AS req_name, + bin_pack.version AS version, + bin_pack.NAME AS bin_name + FROM + ( SELECT id, NAME,version FROM src_pack WHERE {} ) src + LEFT JOIN pack_requires ON src.id = pack_requires.srcIDkey + LEFT JOIN pack_provides ON pack_provides.id = pack_requires.depProIDkey + LEFT JOIN bin_pack ON bin_pack.id = pack_provides.binIDkey + LEFT JOIN src_pack s1 ON s1.id = pack_requires.srcIDkey + LEFT JOIN src_pack s2 ON bin_pack.srcIDkey = s2.id; + """.format(literal_column("name").in_(s_name_set))) + + build_set = data_base.session. \ + execute(sql_com, {'name_{}'.format(i): v + for i, v in enumerate(s_name_set, 1)}).fetchall() + + if not build_set: + continue + + # When processing source package without compilation dependency + to_remove_obj_index = [] + for index, b_pack in enumerate(build_set): + if not b_pack.source_name and not b_pack.req_name: + obj = return_tuple( + b_pack.search_name, + b_pack.source_name, + b_pack.bin_name, + b_pack.version, + db_name, + b_pack.search_version + ) + + build_list.append(obj) + to_remove_obj_index.append(index) + + for i in reversed(to_remove_obj_index): + build_set.pop(i) + + if not build_set: + continue + + build_list.extend([ + return_tuple( + bin_pack.search_name, + bin_pack.source_name, + bin_pack.bin_name, + bin_pack.version, + db_name, + bin_pack.search_version + ) for bin_pack in build_set if bin_pack.bin_id and bin_pack.bin_name + ]) + # Component name can't find its binary package name + not_found_binary.update([(bin_pack.search_name, bin_pack.req_name) + for bin_pack in build_set if not bin_pack.bin_id]) + + s_name_set -= {bin_pack.search_name for bin_pack in build_set + if bin_pack.bin_id} + + if not not_found_binary and not s_name_set: + return ResponseCode.SUCCESS, build_list + + for obj in self.get_binary_in_other_database(not_found_binary, db_name): + build_list.append(obj) + + not_found_binary.clear() + + except AttributeError as attr_error: + current_app.logger.error(attr_error) + except SQLAlchemyError as sql_error: + current_app.logger.error(sql_error) + return ResponseCode.DIS_CONNECTION_DB, None + return ResponseCode.SUCCESS, build_list + + def db_priority(): """ return dbprioty diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 8f78f271..0e1f7630 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -28,7 +28,9 @@ from .serialize import DeletedbSchema from .serialize import InitSystemSchema from .function.install_depend import InstallDepend as installdepend +from .function.build_depend import BuildDepend as builddepend from .serialize import InstallDependSchema +from .serialize import BuildDependSchema from .serialize import have_err_db_name LOGGER = Log(__name__) @@ -293,7 +295,7 @@ class BuildDepend(Resource): changeLog: ''' - def post(self, *args, **kwargs): + def post(self): ''' Description: Query a package's build depend and build depend package's install depend @@ -316,7 +318,42 @@ class BuildDepend(Resource): exception: changeLog: ''' - pass + schema = BuildDependSchema() + + data = request.get_json() + if schema.validate(data): + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + pkg_name = data.get("sourceName") + + db_pri = db_priority() + + if not db_pri: + return jsonify( + ResponseCode.response_json( + ResponseCode.FILE_NOT_FIND_ERROR + ) + ) + + db_list = data.get("db_list") if data.get("db_list") \ + else db_pri + + if have_err_db_name(db_list, db_pri): + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + + build_ins = builddepend([pkg_name], db_list) + + res_code, res_dict, _ = build_ins.build_depend_main() + + return jsonify( + ResponseCode.response_json( + res_code, + data=res_dict if res_dict else None + ) + ) class SelfDepend(Resource): diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index 00e26d42..ae42ad8b 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -75,7 +75,8 @@ class DBHelper(): raise DbnameNoneException( 'The connected database name is empty') self.engine = create_engine( - self.db_type + self.db_name, encoding='utf-8', convert_unicode=True) + self.db_type + self.db_name, encoding='utf-8', convert_unicode=True, + connect_args={'check_same_thread': False}) else: if all([self.user_name, self.passwrod, self.ip_address, self.port, self.db_name]): # create connection object -- Gitee From 938dc15c1a9434e1ab57b29d8b27703be80430a9 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Mon, 29 Jun 2020 17:13:54 +0800 Subject: [PATCH 30/72] add querying bedepend function --- .../apps/package/function/be_depend.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/be_depend.py b/packageship/packageship/application/apps/package/function/be_depend.py index 8740d8b9..e1b7c58d 100644 --- a/packageship/packageship/application/apps/package/function/be_depend.py +++ b/packageship/packageship/application/apps/package/function/be_depend.py @@ -43,7 +43,7 @@ class BeDepend: src_obj = data_base.session.query( src_pack).filter_by(name=self.source_name).first() if src_obj: - # 拼字典 + # spell dictionary self.result_dict[self.source_name + "_src"] = [ "source", src_obj.version, @@ -64,7 +64,6 @@ class BeDepend: :param package_type: package type :return: ''' - # package_type search_set = set(pkg_id_list) id_in = literal_column('id').in_(search_set) # package_type @@ -75,8 +74,8 @@ class BeDepend: src.NAME AS source_name, b2.name AS bin_name, b2.id AS bin_id, - s1.name AS src_name, - s1.id AS src_id, + s1.name AS bebuild_src_name, + s1.id AS bebuild_src_id, s2.name AS install_depend_src_name, s2.id AS install_depend_src_id FROM @@ -94,8 +93,8 @@ class BeDepend: s3.NAME AS source_name, b2.name AS bin_name, b2.id AS bin_id, - s1.name AS src_name, - s1.id AS src_id, + s1.name AS bebuild_src_name, + s1.id AS bebuild_src_id, s2.name AS install_depend_src_name, s2.id AS install_depend_src_id FROM @@ -114,7 +113,7 @@ class BeDepend: search_set, 1)}).fetchall() except SQLAlchemyError as sql_err: current_app.logger.error(sql_err) - return ResponseCode.CONNECT_DB_ERROR, None + return ResponseCode.response_json(ResponseCode.CONNECT_DB_ERROR) if result is None: return @@ -126,14 +125,13 @@ class BeDepend: source_name = 'NOT FOUND' else: source_name = obj.source_name - if obj.src_name: + if obj.bebuild_src_name: # Determine if the source package has been checked - if obj.src_name not in self.source_name_set: - - self.source_name_set.add(obj.src_name) - source_id_list.append(obj.src_id) + if obj.bebuild_src_name not in self.source_name_set: + self.source_name_set.add(obj.bebuild_src_name) + source_id_list.append(obj.bebuild_src_id) - parent_node = obj.src_name + parent_node = obj.bebuild_src_name be_type = "build" # Call the spell dictionary function self.make_dicts( -- Gitee From 777e14c4e7e421d28e68e4e9138e55db33838923 Mon Sep 17 00:00:00 2001 From: gongzt Date: Mon, 29 Jun 2020 19:04:06 +0800 Subject: [PATCH 31/72] Database file name change after successful import --- .../apps/package/function/searchdb.py | 9 ++++++--- .../packageship/application/apps/package/view.py | 5 +++-- .../application/initsystem/data_import.py | 16 ++++++++-------- packageship/packageship/system_config.py | 5 +---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py index 4eaebe7a..18cbdc51 100644 --- a/packageship/packageship/application/apps/package/function/searchdb.py +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -13,11 +13,12 @@ from packageship.libs.dbutils import DBHelper from packageship.libs.log import Log from packageship.application.models.package import bin_pack from packageship.libs.exception import ContentNoneException, Error -from packageship.system_config import DATABASE_SUCCESS_FILE +from packageship.system_config import DATABASE_FILE_INFO from .constants import ResponseCode LOGGER = Log(__name__) + class SearchDB(): """ Description: query in database @@ -57,7 +58,8 @@ class SearchDB(): binary_list.remove(None) search_set = set(binary_list) if not search_set: - LOGGER.logger.warning("The input is None, please check the input value.") + LOGGER.logger.warning( + "The input is None, please check the input value.") return result_list for db_name, data_base in self.db_object_dict.items(): try: @@ -130,12 +132,13 @@ class SearchDB(): return ResponseCode.DIS_CONNECTION_DB, None return ResponseCode.PACK_NAME_NOT_FOUND, None, None, None + def db_priority(): """ return dbprioty """ try: - with open(DATABASE_SUCCESS_FILE, 'r', encoding='utf-8') as file_context: + with open(DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: init_database_date = yaml.load( file_context.read(), Loader=yaml.FullLoader) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 8f78f271..10979e2e 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -14,7 +14,7 @@ from packageship.libs.exception import Error from packageship.libs.exception import ContentNoneException from packageship.libs.exception import DataMergeException from packageship.libs.log import Log -from packageship.system_config import DATABASE_SUCCESS_FILE +from packageship.system_config import DATABASE_FILE_INFO from .function.constants import ResponseCode from .function.packages import get_packages from .function.packages import update_single_package @@ -34,6 +34,7 @@ from .serialize import have_err_db_name LOGGER = Log(__name__) #pylint: disable = no-self-use + class Packages(Resource): ''' Description: interface for package info management @@ -403,7 +404,7 @@ class Repodatas(Resource): changeLog: ''' try: - with open(DATABASE_SUCCESS_FILE, 'r', encoding='utf-8') as file_context: + with open(DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: init_database_date = yaml.load( file_context.read(), Loader=yaml.FullLoader) if init_database_date is None: diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index 2c6a7ff1..e7dafde2 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -20,7 +20,7 @@ from packageship.application.models.temporarydb import src_requires from packageship.application.models.temporarydb import bin_package from packageship.application.models.temporarydb import bin_requiresment from packageship.application.models.temporarydb import bin_provides -from packageship.system_config import DATABASE_SUCCESS_FILE +from packageship.system_config import DATABASE_FILE_INFO from packageship.system_config import DATABASE_FOLDER_PATH LOGGER = Log(__name__) @@ -498,9 +498,9 @@ class InitDataBase(): priority: priority ''' try: - if not os.path.exists(DATABASE_SUCCESS_FILE): - pathlib.Path(DATABASE_SUCCESS_FILE).touch() - with open(DATABASE_SUCCESS_FILE, 'a+', encoding='utf8') as file_context: + if not os.path.exists(DATABASE_FILE_INFO): + pathlib.Path(DATABASE_FILE_INFO).touch() + with open(DATABASE_FILE_INFO, 'a+', encoding='utf8') as file_context: setting_content = [] if 'database_content' in Kwargs.keys(): content = Kwargs.get('database_content') @@ -523,8 +523,8 @@ class InitDataBase(): modify record: ''' try: - if os.path.exists(DATABASE_SUCCESS_FILE): - os.remove(DATABASE_SUCCESS_FILE) + if os.path.exists(DATABASE_FILE_INFO): + os.remove(DATABASE_FILE_INFO) except (IOError, Error) as exception_msg: LOGGER.logger.error(exception_msg) return False @@ -544,14 +544,14 @@ class InitDataBase(): if del_result: try: - file_read = open(DATABASE_SUCCESS_FILE, 'r', encoding='utf-8') + file_read = open(DATABASE_FILE_INFO, 'r', encoding='utf-8') _databases = yaml.load( file_read.read(), Loader=yaml.FullLoader) for database in _databases: if database.get('database_name') == db_name: _databases.remove(database) # Delete the successfully imported database configuration node - with open(DATABASE_SUCCESS_FILE, 'w+', encoding='utf-8') as file_context: + with open(DATABASE_FILE_INFO, 'w+', encoding='utf-8') as file_context: yaml.safe_dump(_databases, file_context) except (IOError, Error) as del_config_error: LOGGER.logger.error(del_config_error) diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py index 26c0dffc..e5a0d53d 100644 --- a/packageship/packageship/system_config.py +++ b/packageship/packageship/system_config.py @@ -19,10 +19,7 @@ SYS_CONFIG_PATH = os.path.join('/', 'etc', 'pkgship', 'package.ini') # data file after successful data import -# DATABASE_SUCCESS_FILE = os.path.join( -# BASE_PATH, 'application', 'initsystem', 'import_success_databse.yaml') - -DATABASE_SUCCESS_FILE = os.path.join( +DATABASE_FILE_INFO = os.path.join( '/', 'var', 'run', 'database_file_info.yaml') # If the path of the imported database is not specified in the configuration file, the -- Gitee From b0b55a90728d9c0a69c1cef0d1a84588a8a3f167 Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Tue, 30 Jun 2020 09:30:59 +0800 Subject: [PATCH 32/72] Modify the branch return value of selfbuild == 1, change the status code return value to None. --- .../application/apps/package/function/build_depend.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py index c4efe919..b751bef0 100644 --- a/packageship/packageship/application/apps/package/function/build_depend.py +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -6,7 +6,6 @@ from packageship.application.apps.package.function.install_depend import Install from packageship.application.apps.package.function.constants import ResponseCode, ListNode - class BuildDepend: """ Find compilation dependency of source package @@ -42,7 +41,7 @@ class BuildDepend: self.self_build(self.pkg_name_list) if None in self.result_dict: del self.result_dict[None] - return ResponseCode.SUCCESS, self.result_dict, self.source_dict + return None, self.result_dict, self.source_dict return ResponseCode.PARAM_ERROR, None, None -- Gitee From 0828e222fb4e7f4aede7db66215199e24a12b0db Mon Sep 17 00:00:00 2001 From: jiang Date: Tue, 30 Jun 2020 11:37:51 +0800 Subject: [PATCH 33/72] The annotation specification follows Google open source project and python style specification --- .../packageship/annotation_specifications.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 packageship/packageship/annotation_specifications.py diff --git a/packageship/packageship/annotation_specifications.py b/packageship/packageship/annotation_specifications.py new file mode 100644 index 00000000..51a2368a --- /dev/null +++ b/packageship/packageship/annotation_specifications.py @@ -0,0 +1,83 @@ +""" +description: Function and class annotation specifications in the project +functions: test +""" +# py文件中,如果全部为函数,顶部信息格式如上,description填写描述信息,functions填写函数名称 +# Args: +# 列出每个参数的名字, 并在名字后使用一个冒号和一个空格, +# 分隔对该参数的描述.如果描述太长超过了单行80字符,使用2或者4个空格的悬挂缩进(与文件其他部分保持一致). +# 描述应该包括所需的类型和含义. +# Returns: +# 描述返回值的类型和语义. 如果函数返回None, 这一部分可以省略. +# Raises: +# 可能产生的异常 + + +def test(name, age): + """ + Description: Function description information + Args: + name: name information + age: age information + Returns: + Returned information + Raises: + IOError: An error occurred accessing the bigtable.Table object. + """ + name = 'tom' + age = 11 + return name, age + + +# description: Function and class annotation specifications in the project +# class: SampleClass +# py文件中,如果全部为类,顶部信息格式如上,description填写描述信息,class填写类名称,用 三引号,不用# +# 类应该在其定义下有一个用于描述该类的文档字符串. +# 如果你的类有公共属性(Attributes), +# 那么文档中应该有一个属性(Attributes)段. +# 并且应该遵守和函数参数相同的格式. + + +class SampleClass(): + """ + Summary of class here. + Longer class information.... + Attributes: + likes_spam: A boolean indicating if we like SPAM or not. + eggs: An integer count of the eggs we have laid. + """ + + def __init__(self, likes_spam=False): + """Inits SampleClass with blah.""" + self.likes_spam = likes_spam + self.eggs = "eggs" + + def public_method_one(self, egg, fun): + """ + Description: Function description information + Args: + egg: egg information + fun: fun information + Returns: + Returned information + Raises: + AttributeError + """ + self.eggs = "eggs" + egg = "egg" + fun = "fun" + return egg, fun + + def public_method_two(self, tom): + """ + Description: Function description information + Args: + tom: tom information + Returns: + Returned information + Raises: + Error + """ + self.likes_spam = True + tom = 'cat' + return tom -- Gitee From e9958ab17ac605b819fac7efee065a00c7dcc4a2 Mon Sep 17 00:00:00 2001 From: jiang Date: Tue, 30 Jun 2020 12:33:46 +0800 Subject: [PATCH 34/72] The annotation specification follows Google open source project and python style specification --- packageship/{packageship => example}/annotation_specifications.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packageship/{packageship => example}/annotation_specifications.py (100%) diff --git a/packageship/packageship/annotation_specifications.py b/packageship/example/annotation_specifications.py similarity index 100% rename from packageship/packageship/annotation_specifications.py rename to packageship/example/annotation_specifications.py -- Gitee From 5b138675ade5185849bfcdd6671acf1035673894 Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Tue, 30 Jun 2020 14:33:45 +0800 Subject: [PATCH 35/72] Modify the selfbuild function, and do not need to return a value. The status code returned by status 1 is success, and at the same time, clear pylint --- .../application/apps/package/function/build_depend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py index b751bef0..693b7df7 100644 --- a/packageship/packageship/application/apps/package/function/build_depend.py +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -41,7 +41,7 @@ class BuildDepend: self.self_build(self.pkg_name_list) if None in self.result_dict: del self.result_dict[None] - return None, self.result_dict, self.source_dict + return ResponseCode.SUCCESS, self.result_dict, self.source_dict return ResponseCode.PARAM_ERROR, None, None @@ -150,15 +150,15 @@ class BuildDepend: :return: """ if not pkg_name_li: - return None + return next_src_set = set() _, bin_info_lis = self.search_db.get_build_depend(pkg_name_li) if not bin_info_lis: - return None + return - # generate data content + # generate data content for obj in bin_info_lis: if not obj.bin_name: @@ -187,4 +187,4 @@ class BuildDepend: self.self_build(next_src_set) - return None + return -- Gitee From 3893c41d854d2672c9df30d4da77550804cad403 Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Tue, 30 Jun 2020 15:16:05 +0800 Subject: [PATCH 36/72] Add a comment that returns the status code success when selfbuild == 1; --- .../application/apps/package/function/build_depend.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py index 693b7df7..38b2a45f 100644 --- a/packageship/packageship/application/apps/package/function/build_depend.py +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -41,6 +41,11 @@ class BuildDepend: self.self_build(self.pkg_name_list) if None in self.result_dict: del self.result_dict[None] + # There are two reasons for the current status code to return SUCCESS + # 1, Other branches return three return values. + # Here, a place holder is needed to prevent unpacking errors during call + # 2, This function is an auxiliary function of other modules. + # The status code is not the final display status code return ResponseCode.SUCCESS, self.result_dict, self.source_dict return ResponseCode.PARAM_ERROR, None, None -- Gitee From 1b70db4c4373482569d51249af3a3c686e3b9b98 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Tue, 30 Jun 2020 15:57:04 +0800 Subject: [PATCH 37/72] Add BeDepend to view --- .../application/apps/package/view.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 72c1600f..d552116c 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -26,7 +26,8 @@ from .serialize import GetpackSchema from .serialize import PutpackSchema from .serialize import DeletedbSchema from .serialize import InitSystemSchema - +from .serialize import BeDependSchema +from .function.be_depend import BeDepend as be_depend from .function.install_depend import InstallDepend as installdepend from .function.build_depend import BuildDepend as builddepend from .serialize import InstallDependSchema @@ -423,7 +424,35 @@ class BeDepend(Resource): exception: changeLog: ''' - pass + schema = BeDependSchema() + data = request.get_json() + validate_err = schema.validate(data) + + if validate_err: + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + + package_name = data.get("packagename") + with_sub_pack = data.get("withsubpack") + db_name = data.get("dbname") + + if db_name not in db_priority(): + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + + bedepnd_ins = be_depend(package_name, db_name, with_sub_pack) + + res_dict = bedepnd_ins.main() + + if not res_dict: + return jsonify( + ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) + ) + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS, data=res_dict) + ) class Repodatas(Resource): -- Gitee From ec1fb10da0f988f14ee567eda911ca55885fca7a Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 16:10:45 +0800 Subject: [PATCH 38/72] add self depend searching function --- .../apps/package/function/searchdb.py | 56 ++++ .../apps/package/function/self_depend.py | 259 ++++++++++++++++++ .../application/apps/package/view.py | 50 +++- 3 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 packageship/packageship/application/apps/package/function/self_depend.py diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py index 4861b86c..a3a1d680 100644 --- a/packageship/packageship/application/apps/package/function/searchdb.py +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -132,6 +132,62 @@ class SearchDB(): return ResponseCode.DIS_CONNECTION_DB, None return ResponseCode.PACK_NAME_NOT_FOUND, None, None, None + def get_sub_pack(self, source_name_list): + """ + Description: get a subpack list based on source name list: + source_name ->source_name_id -> binary_name + input: search package's name, database preority list + return: subpack tuple + changeLog: + """ + if not self.db_object_dict: + return ResponseCode.DIS_CONNECTION_DB, None + + if None in source_name_list: + source_name_list.remove(None) + search_set = set(source_name_list) + result_list = [] + get_list = [] + if not search_set: + return ResponseCode.INPUT_NONE, None + for db_name, data_base in self.db_object_dict.items(): + try: + name_in = literal_column('name').in_(search_set) + sql_com = text('''SELECT + t1.NAME as subpack_name, + t2.version as search_version, + t2.NAME as search_name + FROM bin_pack t1, src_pack t2 + WHERE + t2.id = t1.srcIDkey + AND t2.{} + '''.format(name_in)) + subpack_tuple = data_base.session. \ + execute(sql_com, {'name_{}'.format(i): v + for i, v in enumerate(search_set, 1)}).fetchall() + if subpack_tuple: + for result in subpack_tuple: + result_list.append((result, db_name)) + get_list.append(result.search_name) + search_set.symmetric_difference_update(set(get_list)) + get_list.clear() + if not search_set: + return ResponseCode.SUCCESS, result_list + else: + continue + except AttributeError as attr_error: + current_app.logger.error(attr_error) + except SQLAlchemyError as sql_error: + current_app.logger.error(sql_error) + return_tuple = namedtuple( + 'return_tuple', 'subpack_name search_version search_name') + for search_name in search_set: + LOGGER.logger.warning("Can't not find " + + search_name + " subpack in all database") + result_list.append( + (return_tuple(None, None, search_name), 'NOT_FOUND')) + return ResponseCode.SUCCESS, result_list + def get_binary_in_other_database(self, not_found_binary, db_): """ Binary package name data not found in the current database, go to other databases to try diff --git a/packageship/packageship/application/apps/package/function/self_depend.py b/packageship/packageship/application/apps/package/function/self_depend.py new file mode 100644 index 00000000..d2bfc677 --- /dev/null +++ b/packageship/packageship/application/apps/package/function/self_depend.py @@ -0,0 +1,259 @@ +''' + Querying for self dependencies + Querying packages install and build depend for those package can be + build and install +''' + +import copy +from packageship.libs.log import Log +from .searchdb import SearchDB +from .constants import ResponseCode +from .constants import ListNode +from .install_depend import InstallDepend as install_depend +from .build_depend import BuildDepend as build_depend + +LOGGER = Log(__name__) + +class SelfDepend(): + ''' + Querying for self dependencies + Querying packages install and build depend for those package can be + build and install + ''' + def __init__(self, db_list): + self.binary_dict = DictionaryOperations() + self.source_dicts = DictionaryOperations() + self.result_tmp = dict() + self.search_install_list = [] + self.search_build_list = [] + self.search_subpack_list = [] + self.withsubpack = 0 + self.db_list = db_list + self.search_db = SearchDB(db_list) + + def query_depend(self, packname, selfbuild, withsubpack, packtype='binary'): + """ + Description: init result dict and determint the loop end point + input: packname, selfbuild, withsubpack, packtype + return: result dicts, source dicts + changeLog: + """ + if not self.search_db.db_object_dict: + return ResponseCode.DIS_CONNECTION_DB, None, None + if not packname: + return ResponseCode.INPUT_NONE + + self.withsubpack = withsubpack + response_code = self.init_dict(packname, packtype) + if response_code != ResponseCode.SUCCESS: + return response_code, self.binary_dict.dictionary, self.source_dicts.dictionary + + for key, _ in self.binary_dict.dictionary.items(): + self.search_install_list.append(key) + for key, _ in self.source_dicts.dictionary.items(): + self.search_build_list.append(key) + if self.withsubpack == 1: + self.search_subpack_list.append(key) + + while self.search_build_list or self.search_install_list or self.search_subpack_list: + if self.search_install_list: + self.query_install() + if self.withsubpack == 1 and self.search_subpack_list: + self.with_subpack() + if self.search_build_list: + self.query_build(selfbuild) + return response_code, self.binary_dict.dictionary, self.source_dicts.dictionary + + def init_dict(self, packname, packtype): + """ + Description: init result dict + input: packname, packtype + return: response_code + changeLog: + """ + if packtype == 'source': + response_code, subpack_list = self.search_db.get_sub_pack([packname]) + if subpack_list: + for subpack_tuple, dbname in subpack_list: + self.source_dicts.append_src(packname, dbname, subpack_tuple.search_version) + if dbname != 'NOT_FOUND': + self.binary_dict.append_bin(key=subpack_tuple.subpack_name, + src=packname, + version=subpack_tuple.search_version, + dbname=dbname) + else: + return ResponseCode.PACK_NAME_NOT_FOUND + + else: + response_code, dbname, source_name, version = \ + self.search_db.get_src_name(packname) + if response_code != ResponseCode.SUCCESS: + return response_code + self.source_dicts.append_src(source_name, dbname, version) + self.binary_dict.append_bin(key=packname, + src=source_name, + version=version, + dbname=dbname) + return response_code + + def query_install(self): + """ + Description: query install depend + return: + changeLog: + """ + self.result_tmp.clear() + _, self.result_tmp = \ + install_depend(self.db_list).query_install_depend(self.search_install_list, + self.binary_dict.dictionary) + self.search_install_list.clear() + for key, values in self.result_tmp.items(): + if key in self.binary_dict.dictionary: + if ['root', None] in values[ListNode.PARENT_LIST]: + index = values[ListNode.PARENT_LIST].index(['root', None]) + del values[ListNode.PARENT_LIST][index] + self.binary_dict.update_value(key=key, parent_list=values[ListNode.PARENT_LIST]) + else: + if not key: + continue + self.binary_dict.dictionary[key] = copy.deepcopy(values) + source_name = values[ListNode.SOURCE_NAME] + if not source_name: + LOGGER.logger.warning("source name is None") + if source_name and source_name not in self.source_dicts.dictionary: + self.source_dicts.append_src(key=source_name, + dbname=values[ListNode.DBNAME], + version=values[ListNode.VERSION]) + self.search_build_list.append(source_name) + if self.withsubpack == 1: + self.search_subpack_list.append(source_name) + + def with_subpack(self): + """ + Description: query subpackage + return: + changeLog: + """ + if None in self.search_subpack_list: + LOGGER.logger.warning("There is a NONE in input value:" + \ + str(self.search_subpack_list)) + self.search_subpack_list.remove(None) + _, result_list = self.search_db.get_sub_pack(self.search_subpack_list) + for subpack_tuple, dbname in result_list: + if dbname != 'NOT_FOUND': + if subpack_tuple.subpack_name not in self.binary_dict.dictionary: + self.binary_dict.append_bin(key=subpack_tuple.subpack_name, + src=subpack_tuple.search_name, + version=subpack_tuple.search_version, + dbname=dbname, + parent_node=[subpack_tuple.search_name, 'Subpack']) + self.search_install_list.append(subpack_tuple.subpack_name) + self.search_subpack_list.clear() + + def query_build(self, selfbuild): + """ + Description: query build depend + return: + changeLog: + """ + self.result_tmp.clear() + if selfbuild == 0: + self.query_builddep() + else: + self.query_selfbuild() + + def query_builddep(self): + """ + Description: for selfbuild == 0, query selfbuild depend + return: + changeLog: + """ + _, self.result_tmp, _ = build_depend( + self.search_build_list, + self.db_list, + self_build=0, + history_dict=self.binary_dict.dictionary + ).build_depend_main() + + self.search_build_list.clear() + for key, values in self.result_tmp.items(): + if not key: + LOGGER.logger.warning("key is NONE for value = " + str(values)) + continue + if key not in self.binary_dict.dictionary and values[0] != 'source': + self.binary_dict.dictionary[key] = copy.deepcopy(values) + if self.withsubpack == 1: + source_name = values[ListNode.SOURCE_NAME] + if not source_name: + LOGGER.logger.warning("source name is None") + if source_name and source_name not in self.source_dicts.dictionary: + self.source_dicts.append_src(key=source_name, + dbname=values[ListNode.DBNAME], + version=values[ListNode.VERSION]) + self.search_subpack_list.append(source_name) + elif key in self.binary_dict.dictionary: + self.binary_dict.update_value(key=key, parent_list=values[ListNode.PARENT_LIST]) + + def query_selfbuild(self): + """ + Description: for selfbuild == 1, query selfbuild depend + return: + changeLog: + """ + _, self.result_tmp, source_dicts_tmp = build_depend( + self.search_build_list, + self.db_list, + self_build=1, + history_dict=self.source_dicts.dictionary + ).build_depend_main() + + for key, values in self.result_tmp.items(): + if not key: + LOGGER.logger.warning("key is NONE for value = " + str(values)) + continue + if key in self.binary_dict.dictionary: + self.binary_dict.update_value(key=key, parent_list=values[ListNode.PARENT_LIST]) + else: + self.binary_dict.dictionary[key] = copy.deepcopy(values) + self.search_install_list.append(key) + for key, values in source_dicts_tmp.items(): + if not key: + LOGGER.logger.warning("key is NONE for value = " + str(values)) + continue + if key not in self.source_dicts.dictionary: + self.source_dicts.dictionary[key] = copy.deepcopy(values) + if self.with_subpack == 1: + self.search_subpack_list.append(key) + self.search_build_list.clear() + + +class DictionaryOperations(): + ''' + Related to dictionary operations, creating dictionary, append dictionary + ''' + + def __init__(self): + + self.dictionary = dict() + + def append_src(self, key, dbname, version): + ''' + Appending source dictionary + ''' + self.dictionary[key] = [dbname, version] + + def append_bin(self, key, src=None, version=None, dbname=None, parent_node=None): + ''' + Appending binary dictionary + ''' + if not parent_node: + self.dictionary[key] = [src, version, dbname, [['root', None]]] + else: + self.dictionary[key] = [src, version, dbname, [parent_node]] + + def update_value(self, key, parent_list=None): + ''' + append dictionary + ''' + if parent_list: + self.dictionary[key][ListNode.PARENT_LIST].extend(parent_list) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 72c1600f..427dc19c 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -29,8 +29,10 @@ from .serialize import InitSystemSchema from .function.install_depend import InstallDepend as installdepend from .function.build_depend import BuildDepend as builddepend +from .function.self_depend import SelfDepend as self_depend from .serialize import InstallDependSchema from .serialize import BuildDependSchema +from .serialize import SelfDependSchema from .serialize import have_err_db_name LOGGER = Log(__name__) @@ -365,7 +367,7 @@ class SelfDepend(Resource): changeLog: ''' - def post(self, *args, **kwargs): + def post(self): ''' description: Query a package's all dependencies including install and build depend (support quering a binary or source package in one or more databases) @@ -390,7 +392,51 @@ class SelfDepend(Resource): exception: changeLog: ''' - pass + schema = SelfDependSchema() + + data = request.get_json() + validate_err = schema.validate(data) + + if validate_err: + return jsonify( + ResponseCode.response_json(ResponseCode.PARAM_ERROR) + ) + + pkg_name = data.get("packagename") + db_pri = db_priority() + + if not db_pri: + return jsonify( + ResponseCode.response_json( + ResponseCode.FILE_NOT_FIND_ERROR + ) + ) + db_list = data.get("db_list") if data.get("db_list") \ + else db_pri + + self_build = data.get("selfbuild", 0) + with_sub_pack = data.get("withsubpack", 0) + pack_type = data.get("packtype", "binary") + + if have_err_db_name(db_list, db_pri): + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + response_code, binary_dicts, source_dicts = \ + self_depend(db_list).query_depend(pkg_name, int(self_build), + int(with_sub_pack), pack_type) + + if not all([binary_dicts, source_dicts]): + return jsonify( + ResponseCode.response_json(response_code) + ) + + return jsonify( + ResponseCode.response_json(ResponseCode.SUCCESS, data={ + "binary_dicts": binary_dicts, + "source_dicts": source_dicts + }) + ) class BeDepend(Resource): -- Gitee From 3087f3589e7c7280ab7463a4186c98e58180f878 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 19:54:48 +0800 Subject: [PATCH 39/72] change file name in setup.py --- packageship/setup.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/packageship/setup.py b/packageship/setup.py index 958ce731..34726b5a 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -17,13 +17,13 @@ setup( 'packageship.application.apps.package.serialize', 'packageship.application.apps.package.url', 'packageship.application.apps.package.view', - 'packageship.application.apps.package.function.BeDepend', - 'packageship.application.apps.package.function.BuildDepend', - 'packageship.application.apps.package.function.Constants', - 'packageship.application.apps.package.function.InstallDepend', - 'packageship.application.apps.package.function.Packages', - 'packageship.application.apps.package.function.SearchDB', - 'packageship.application.apps.package.function.SelfDepend', + 'packageship.application.apps.package.function.be_depend', + 'packageship.application.apps.package.function.build_depend', + 'packageship.application.apps.package.function.constants', + 'packageship.application.apps.package.function.install_depend', + 'packageship.application.apps.package.function.packages', + 'packageship.application.apps.package.function.searchdb', + 'packageship.application.apps.package.function.self_depend', 'packageship.application.initsystem.data_import', 'packageship.application.initsystem.datamerge', 'packageship.application.models.package', @@ -33,16 +33,7 @@ setup( 'packageship.libs.configutils.readconfig', 'packageship.libs.dbutils.sqlalchemy_helper', 'packageship.libs.exception.ext', - 'packageship.libs.log.loghelper', - 'packageship.tests.test_build_depend', - 'packageship.tests.test_delete_repodatas', - 'packageship.tests.test_get_repodatas', - 'packageship.tests.test_get_singlepack', - 'packageship.tests.test_install_depend', - 'packageship.tests.test_packages', - 'packageship.tests.test_self_depend', - 'packageship.tests.test_update_singlepack', - 'packageship.tests.test_importdata', + 'packageship.libs.log.loghelper' 'packageship.manage', 'packageship.cli', 'packageship.selfpkg', -- Gitee From d871d5271df355556e0f5bfcb18ed55059f37540 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 19:57:49 +0800 Subject: [PATCH 40/72] add , --- packageship/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packageship/setup.py b/packageship/setup.py index 34726b5a..ac7acba1 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -33,7 +33,7 @@ setup( 'packageship.libs.configutils.readconfig', 'packageship.libs.dbutils.sqlalchemy_helper', 'packageship.libs.exception.ext', - 'packageship.libs.log.loghelper' + 'packageship.libs.log.loghelper', 'packageship.manage', 'packageship.cli', 'packageship.selfpkg', -- Gitee From c2c12dd70aec78f898072c3220b8c788e5898a51 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 20:02:05 +0800 Subject: [PATCH 41/72] change cli to pkgship --- packageship/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packageship/setup.py b/packageship/setup.py index ac7acba1..8059d79d 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -35,7 +35,7 @@ setup( 'packageship.libs.exception.ext', 'packageship.libs.log.loghelper', 'packageship.manage', - 'packageship.cli', + 'packageship.pkgship', 'packageship.selfpkg', 'packageship.system_config'], requires=['prettytable (==0.7.2)', -- Gitee From f7f6cc527dac30736b59d74af211ba7e436eeca5 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 20:08:10 +0800 Subject: [PATCH 42/72] add pkgshipd file in spec --- packageship/pkgship.spec | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packageship/pkgship.spec b/packageship/pkgship.spec index d2748489..04bcbd34 100644 --- a/packageship/pkgship.spec +++ b/packageship/pkgship.spec @@ -49,9 +49,13 @@ rm -rf %{python3_sitelib}/packageship/build %{python3_sitelib}/packageship/dist %doc README.md %{python3_sitelib}/* %config %{_sysconfdir}/pkgship/* +%attr(0755,root,root) %{_bindir}/pkgshipd %changelog +* Tue JUN 30 2020 Yiru Wang +- add pkgshipd file + * Tue Jun 11 2020 Feng Hu - add macro to build cli bin when rpm install -- Gitee From 66911470ad98a77dbfac9e552e71b22e7d66dee9 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 20:29:01 +0800 Subject: [PATCH 43/72] change cli.py to pkgship.py --- packageship/pkgship.spec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packageship/pkgship.spec b/packageship/pkgship.spec index 04bcbd34..ce6adde1 100644 --- a/packageship/pkgship.spec +++ b/packageship/pkgship.spec @@ -1,6 +1,6 @@ Name: pkgship Version: 1.0 -Release: 0 +Release: 1 Summary: Pkgship implements rpm package dependence ,maintainer, patch query and so no. License: Mulan 2.0 URL: https://gitee.com/openeuler/openEuler-Advisor @@ -36,10 +36,10 @@ fi cd %{python3_sitelib}/packageship/ -/usr/local/bin/pyinstaller -F cli.py -sed -i "s/hiddenimports\=\[\]/hiddenimports\=\['pkg_resources.py2_warn'\]/g" cli.spec -/usr/local/bin/pyinstaller cli.spec -cp dist/cli /usr/bin/ +/usr/local/bin/pyinstaller -F pkgship.py +sed -i "s/hiddenimports\=\[\]/hiddenimports\=\['pkg_resources.py2_warn'\]/g" pkgship.spec +/usr/local/bin/pyinstaller pkgship.spec +cp dist/pkgship /usr/bin/ rm -rf %{python3_sitelib}/packageship/build %{python3_sitelib}/packageship/dist %postun -- Gitee From e16e766d0c69aec770ae3f6bc85aa7e2cd9b19f6 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Tue, 30 Jun 2020 21:34:16 +0800 Subject: [PATCH 44/72] change Constant to constant --- packageship/packageship/package.ini | 2 +- packageship/packageship/pkgship.py | 4 ++-- packageship/pkgship.spec | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packageship/packageship/package.ini b/packageship/packageship/package.ini index fe0602da..96d09e2e 100644 --- a/packageship/packageship/package.ini +++ b/packageship/packageship/package.ini @@ -63,7 +63,7 @@ log_name=log_info.log [UWSGI] -daemonize=/var/run/pkgship/uwsgi.log +daemonize=/var/log/uwsgi.log buffer-size=65536 diff --git a/packageship/packageship/pkgship.py b/packageship/packageship/pkgship.py index e5e8534a..c22a5409 100644 --- a/packageship/packageship/pkgship.py +++ b/packageship/packageship/pkgship.py @@ -18,8 +18,8 @@ except ImportError as import_error: print('Error importing related dependencies, \ please check if related dependencies are installed') else: - from packageship.application.apps.package.function.Constants import ResponseCode - from packageship.application.apps.package.function.Constants import ListNode + from packageship.application.apps.package.function.constants import ResponseCode + from packageship.application.apps.package.function.constants import ListNode DB_NAME = 0 diff --git a/packageship/pkgship.spec b/packageship/pkgship.spec index ce6adde1..c76911ca 100644 --- a/packageship/pkgship.spec +++ b/packageship/pkgship.spec @@ -30,8 +30,8 @@ Pkgship implements rpm package dependence ,maintainer, patch query and so no. %post #build cli bin -if [ -f "/usr/bin/cli" ];then - rm -rf /usr/bin/cli +if [ -f "/usr/bin/pkgship" ];then + rm -rf /usr/bin/pkgship fi -- Gitee From 31c59bd78bc63197dc0476f44ee1dd72d8e65fdc Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Wed, 1 Jul 2020 17:01:12 +0800 Subject: [PATCH 45/72] fix the bug that if the packageship dir exsit in the path where the uwsgi service start command execute, the python would run the packageship code under that path --- packageship/packageship/pkgshipd | 42 +++++++++++++++++++------------- packageship/pkgship.spec | 6 ++--- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packageship/packageship/pkgshipd b/packageship/packageship/pkgshipd index f50d4ded..1425581f 100755 --- a/packageship/packageship/pkgshipd +++ b/packageship/packageship/pkgshipd @@ -6,7 +6,7 @@ if [ ! -d "$OUT_PATH" ]; then fi if [ ! -f "$SYS_PATH/package.ini" ]; then - echo "!!!$SYS_PATH/package.ini dose not exist!!!" + echo "[ERROR] $SYS_PATH/package.ini dose not exist!!!" exit 0 fi @@ -15,7 +15,7 @@ function get_config(){ } function create_config_file(){ - echo "config type is: $service" + echo "[INFO] config type is: $service" daemonize=$(get_config "$service" "daemonize") buffer_size=$(get_config "$service" "buffer-size") wsgi_file_path=$(find /usr/lib/ -name "packageship") @@ -23,15 +23,15 @@ function create_config_file(){ write_port=$(get_config "$service" "write_port") write_ip_addr=$(get_config "$service" "write_ip_addr") if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$write_ip_addr" ]] || [[ -z "$write_port" ]];then - echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" - echo "!!!The following config name is needed: daemonize, buffer_size, write_port and write_ip_addr!!!" + echo "[ERROR] CAN NOT find all config name in: $SYS_PATH/package.ini, Please check the file" + echo "[ERROR] The following config name is needed: daemonize, buffer_size, write_port and write_ip_addr" exit 1 fi if [ -z "$wsgi_file_path" ];then - echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" + echo "[ERROR] CAN NOT find the wsgi file path under: /usr/lib/" exit 1 fi - echo "manage.ini is saved to $OUT_PATH/manage.ini" + echo "[INFO] manage.ini is saved to $OUT_PATH/manage.ini" echo "[uwsgi] http=$write_ip_addr:$write_port module=packageship.manage @@ -47,16 +47,16 @@ daemonize=$daemonize" > $OUT_PATH/manage.ini query_ip_addr=$(get_config "$service" "query_ip_addr") if [[ -z "$daemonize" ]] || [[ -z "$buffer_size" ]] || [[ -z "$query_ip_addr" ]] || [[ -z "$query_port" ]];then - echo "!!!CAN NOT find all config name in $SYS_PATH/package.ini, Please check the file!!!" - echo "!!!The following config name is needed: daemonize, buffer_size, query_port and query_ip_addr!!!" + echo "[ERROR] CAN NOT find all config name in: $SYS_PATH/package.ini, Please check the file." + echo "[ERROR] The following config name is needed: daemonize, buffer_size, query_port and query_ip_addr." exit 1 fi if [ -z "$wsgi_file_path" ];then - echo "!!!CAN NOT find the wsgi file path under /usr/lib/!!!" + echo "[ERROR] CAN NOT find the wsgi file path under: /usr/lib/" exit 1 fi - echo "selfpkg.ini is saved to $OUT_PATH/selfpkg.ini" + echo "[INFO] selfpkg.ini is saved to: $OUT_PATH/selfpkg.ini" echo "[uwsgi] http=$query_ip_addr:$query_port module=packageship.selfpkg @@ -73,19 +73,27 @@ daemonize=$daemonize" > $OUT_PATH/selfpkg.ini function start_service(){ if [ "`ps aux | grep "uwsgi" | grep "$1.ini"`" != "" ];then - echo "!!!$1 service is running, please stop it first!!!" + echo "[WARNING] $1 service is running, please STOP it first." else + cd $wsgi_file_path uwsgi -d --ini $OUT_PATH/$1.ini + echo "[INFO] START uwsgi service: $1.ini" fi } function stop_service(){ + if [ ! -f "$OUT_PATH/$1.pid" ]; then + echo "[ERROR] STOP service FAILED, $OUT_PATH/$1.pid dose not exist." + echo "[ERROR] Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]" + exit 0 + fi + pid=$(cat $OUT_PATH/$1.pid) if [ "`ps aux | awk 'BEGIN{FS=" "}{if ($2=='$pid') print $0}' | grep "$1.ini"`" != "" ];then uwsgi --$2 $OUT_PATH/$1.pid + echo "[INFO] STOP uwsgi service: $1.ini" else - echo "!!!STOP service [FAILED], Please start the service first!!!" - echo "===If the service has already exist, Please stop it manually by using [ps -aux] and [uwsgi --stop #PID]===" + echo "[WARNING] STOP service [FAILED], Please START the service first." fi } @@ -100,7 +108,7 @@ if [ X$2 = X ];then elif [ $2 = "manage" -o $2 = "selfpkg" ];then service=$2 else - echo "!!!can not phase the input of $2!!!" + echo "[ERROR] Can not phase the input of $2!!!" exit 0 fi @@ -117,7 +125,7 @@ then else start_service $service fi - echo "===The run log is saved into $daemonize===" + echo "===The run log is saved into: $daemonize===" elif [ $1 = stop ];then if [ $service = "all" ];then @@ -126,7 +134,7 @@ elif [ $1 = stop ];then else stop_service $service "stop" fi - echo "===The run log is saved into $daemonize===" + echo "===The run log is saved into: $daemonize===" elif [ $1 = restart ];then if [ $service = "all" ];then @@ -135,7 +143,7 @@ elif [ $1 = restart ];then else stop_service $service "reload" fi - echo "===The run log is saved into $daemonize===" + echo "===The run log is saved into: $daemonize===" else echo "Usages: sh pkgshipd.sh start|stop|restart [manage|selfpkg]" diff --git a/packageship/pkgship.spec b/packageship/pkgship.spec index c76911ca..9b48b7ad 100644 --- a/packageship/pkgship.spec +++ b/packageship/pkgship.spec @@ -53,11 +53,11 @@ rm -rf %{python3_sitelib}/packageship/build %{python3_sitelib}/packageship/dist %changelog -* Tue JUN 30 2020 Yiru Wang +* Tue JUN 30 2020 Yiru Wang - 1.0-1 - add pkgshipd file -* Tue Jun 11 2020 Feng Hu +* Tue Jun 11 2020 Feng Hu - 1.0-0 - add macro to build cli bin when rpm install -* Sat Jun 6 2020 Feng Hu +* Sat Jun 6 2020 Feng Hu - 1.0-0 - init package -- Gitee From f1c2f4364834e4451b7e5e140faf15ecafb462ce Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Wed, 1 Jul 2020 20:32:53 +0800 Subject: [PATCH 46/72] Add the overall directory structure of unit test cases, and decouple the strong dependence between test cases and system initialization success --- .../libs/dbutils/sqlalchemy_helper.py | 4 +- .../{test_install_depend.py => __init__.py} | 0 packageship/test/base_code/__init__.py | 1 + .../test/base_code/common_test_code.py | 48 +++++ .../test/base_code/init_config_path.py | 37 ++++ .../test/base_code/operate_data_base.py | 34 +++ packageship/test/base_code/read_data_base.py | 31 +++ packageship/test/common_files/conf.yaml | 14 ++ .../build_depend.json | 91 ++++++++ .../test/common_files/database_file_info.yaml | 6 + .../common_files/db_origin/data_1_bin.sqlite | Bin 0 -> 40960 bytes .../common_files/db_origin/data_1_src.sqlite | Bin 0 -> 28672 bytes .../common_files/db_origin/data_2_bin.sqlite | Bin 0 -> 40960 bytes .../common_files/db_origin/data_2_src.sqlite | Bin 0 -> 28672 bytes .../dbs/maintenance.information.db | Bin 0 -> 8192 bytes .../common_files/dbs/openEuler-20.03-LTS.db | Bin 0 -> 20480 bytes .../common_files/dbs/openEuler-20.04-LTS.db | Bin 0 -> 20480 bytes .../operate_dbs/maintenance.information.db | Bin 0 -> 8192 bytes .../operate_dbs/openEuler-20.03-LTS.db | Bin 0 -> 20480 bytes .../operate_dbs/openEuler-20.04-LTS.db | Bin 0 -> 20480 bytes packageship/test/common_files/package.ini | 24 +++ packageship/test/run_tests.py | 20 ++ packageship/test/test_module/__init__.py | 0 .../dependent_query_tests/__init__.py | 0 .../test_build_depend.py | 164 ++++++++++++++ .../test_module/init_system_tests/__init__.py | 0 .../init_system_tests/test_importdata.py | 203 ++++++++++++++++++ .../test_module/packages_tests/__init__.py | 0 .../packages_tests/test_packages.py | 89 ++++++++ 29 files changed, 764 insertions(+), 2 deletions(-) rename packageship/test/{test_install_depend.py => __init__.py} (100%) create mode 100644 packageship/test/base_code/__init__.py create mode 100644 packageship/test/base_code/common_test_code.py create mode 100644 packageship/test/base_code/init_config_path.py create mode 100644 packageship/test/base_code/operate_data_base.py create mode 100644 packageship/test/base_code/read_data_base.py create mode 100644 packageship/test/common_files/conf.yaml create mode 100644 packageship/test/common_files/correct_test_result_json/build_depend.json create mode 100644 packageship/test/common_files/database_file_info.yaml create mode 100644 packageship/test/common_files/db_origin/data_1_bin.sqlite create mode 100644 packageship/test/common_files/db_origin/data_1_src.sqlite create mode 100644 packageship/test/common_files/db_origin/data_2_bin.sqlite create mode 100644 packageship/test/common_files/db_origin/data_2_src.sqlite create mode 100644 packageship/test/common_files/dbs/maintenance.information.db create mode 100644 packageship/test/common_files/dbs/openEuler-20.03-LTS.db create mode 100644 packageship/test/common_files/dbs/openEuler-20.04-LTS.db create mode 100644 packageship/test/common_files/operate_dbs/maintenance.information.db create mode 100644 packageship/test/common_files/operate_dbs/openEuler-20.03-LTS.db create mode 100644 packageship/test/common_files/operate_dbs/openEuler-20.04-LTS.db create mode 100644 packageship/test/common_files/package.ini create mode 100644 packageship/test/run_tests.py create mode 100644 packageship/test/test_module/__init__.py create mode 100644 packageship/test/test_module/dependent_query_tests/__init__.py create mode 100644 packageship/test/test_module/dependent_query_tests/test_build_depend.py create mode 100644 packageship/test/test_module/init_system_tests/__init__.py create mode 100644 packageship/test/test_module/init_system_tests/test_importdata.py create mode 100644 packageship/test/test_module/packages_tests/__init__.py create mode 100644 packageship/test/test_module/packages_tests/test_packages.py diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index ae42ad8b..93097a61 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -14,7 +14,7 @@ from packageship.libs.exception.ext import Error from packageship.libs.exception.ext import DbnameNoneException from packageship.libs.exception.ext import ContentNoneException from packageship.libs.configutils.readconfig import ReadConfig -from packageship.system_config import DATABASE_FOLDER_PATH +from packageship import system_config class DBHelper(): @@ -99,7 +99,7 @@ class DBHelper(): self.database_file_path = self._readconfig.get_system( 'data_base_path') if not self.database_file_path: - self.database_file_path = DATABASE_FOLDER_PATH + self.database_file_path = system_config.DATABASE_FOLDER_PATH if not os.path.exists(self.database_file_path): os.makedirs(self.database_file_path) diff --git a/packageship/test/test_install_depend.py b/packageship/test/__init__.py similarity index 100% rename from packageship/test/test_install_depend.py rename to packageship/test/__init__.py diff --git a/packageship/test/base_code/__init__.py b/packageship/test/base_code/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packageship/test/base_code/__init__.py @@ -0,0 +1 @@ + diff --git a/packageship/test/base_code/common_test_code.py b/packageship/test/base_code/common_test_code.py new file mode 100644 index 00000000..39ce8db0 --- /dev/null +++ b/packageship/test/base_code/common_test_code.py @@ -0,0 +1,48 @@ +# -*- coding:utf-8 -*- +""" +Compare the values in two Python data types for equality, ignoring the order of values +""" + +import os +import json +from packageship.system_config import BASE_PATH + + +def compare_two_values(obj1, obj2): + """ + + Args: + obj1:object1 It can be a data type in Python, + and can be converted by using the str() method + obj2:object2 same as obj1 + + Returns: True or False + + """ + # With the help of the str() method provided by python,It's so powerful + + return obj1 == obj2 or (isinstance(obj1, type(obj2)) and + "".join(sorted(str(obj1))) == "".join(sorted(str(obj2)))) + + +def get_correct_json_by_filename(filename): + """ + + Args: + filename: Correct JSON file name without suffix + + Returns: list this json file's content + + """ + json_path = os.path.join(os.path.dirname(BASE_PATH), + "test", + "common_files", + "correct_test_result_json", + f"{filename}.json") + try: + with open(json_path, "r") as json_fp: + correct_list = json.loads(json_fp.read()) + except FileNotFoundError: + return [] + + return correct_list diff --git a/packageship/test/base_code/init_config_path.py b/packageship/test/base_code/init_config_path.py new file mode 100644 index 00000000..82103fd4 --- /dev/null +++ b/packageship/test/base_code/init_config_path.py @@ -0,0 +1,37 @@ +# -*- coding:utf-8 -*- + +import os +from configparser import ConfigParser +from packageship import system_config +import yaml + + +class InitConf: + def __init__(self): + base_path = os.path.join(os.path.dirname(system_config.BASE_PATH), + "test", + "common_files") + config = ConfigParser() + config.read(system_config.SYS_CONFIG_PATH) + + conf_path = os.path.join(base_path, "conf.yaml") + + config.set("SYSTEM", "init_conf_path", conf_path) + config.write(open(system_config.SYS_CONFIG_PATH, "w")) + + with open(conf_path, 'r', encoding='utf-8') as f: + origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + + for index, obj in enumerate(origin_yaml, 1): + src_path = os.path.join(base_path, "db_origin", f"data_{str(index)}_src.sqlite") + bin_path = os.path.join(base_path, "db_origin", f"data_{str(index)}_bin.sqlite") + obj["src_db_file"] = [src_path] + obj["bin_db_file"] = [bin_path] + with open(conf_path, 'w', encoding='utf-8') as w_f: + yaml.dump(origin_yaml, w_f) + + +# A simple method of single case model +# Prevent multiple file modifications + +init_config = InitConf() diff --git a/packageship/test/base_code/operate_data_base.py b/packageship/test/base_code/operate_data_base.py new file mode 100644 index 00000000..c3d18e82 --- /dev/null +++ b/packageship/test/base_code/operate_data_base.py @@ -0,0 +1,34 @@ +# -*- coding:utf-8 -*- +import os +import unittest + +try: + from packageship import system_config + + system_config.SYS_CONFIG_PATH = os.path.join(os.path.dirname(system_config.BASE_PATH), + 'test', + 'common_files', + 'package.ini') + + system_config.DATABASE_FILE_INFO = os.path.join(os.path.dirname(system_config.BASE_PATH), + 'test', + 'common_files', + 'database_file_info.yaml') + system_config.DATABASE_FOLDER_PATH = os.path.join(os.path.dirname(system_config.BASE_PATH), + 'test', + 'common_files', + 'operate_dbs') + + from test.base_code.init_config_path import init_config + from packageship.manage import app +except Exception as e: + raise + + +class OperateTestBase(unittest.TestCase): + """ + + """ + + def setUp(self) -> None: + self.client = app.test_client() diff --git a/packageship/test/base_code/read_data_base.py b/packageship/test/base_code/read_data_base.py new file mode 100644 index 00000000..d183d44d --- /dev/null +++ b/packageship/test/base_code/read_data_base.py @@ -0,0 +1,31 @@ +# -*- coding:utf-8 -*- +import os +import unittest + +try: + from packageship import system_config + + system_config.SYS_CONFIG_PATH = os.path.join(os.path.dirname(system_config.BASE_PATH), + 'test', + 'common_files', + 'package.ini') + + system_config.DATABASE_FILE_INFO = os.path.join(os.path.dirname(system_config.BASE_PATH), + 'test', + 'common_files', + 'database_file_info.yaml') + system_config.DATABASE_FOLDER_PATH = os.path.join(os.path.dirname(system_config.BASE_PATH), + 'test', + 'common_files', + 'dbs') + + from test.base_code.init_config_path import init_config + from packageship.selfpkg import app + +except Exception as e: + raise + + +class ReadTestBase(unittest.TestCase): + def setUp(self) -> None: + self.client = app.test_client() diff --git a/packageship/test/common_files/conf.yaml b/packageship/test/common_files/conf.yaml new file mode 100644 index 00000000..0aeea894 --- /dev/null +++ b/packageship/test/common_files/conf.yaml @@ -0,0 +1,14 @@ +- bin_db_file: + - C:\Users\14500\Desktop\workspace\openEuler_pkgmnt\openEuler_Advisor\packageship\test\common_files\db_origin\data_1_bin.sqlite + dbname: openEuler-20.03-LTS + priority: 1 + src_db_file: + - C:\Users\14500\Desktop\workspace\openEuler_pkgmnt\openEuler_Advisor\packageship\test\common_files\db_origin\data_1_src.sqlite + status: enable +- bin_db_file: + - C:\Users\14500\Desktop\workspace\openEuler_pkgmnt\openEuler_Advisor\packageship\test\common_files\db_origin\data_2_bin.sqlite + dbname: openEuler-20.04-LTS + priority: 2 + src_db_file: + - C:\Users\14500\Desktop\workspace\openEuler_pkgmnt\openEuler_Advisor\packageship\test\common_files\db_origin\data_2_src.sqlite + status: enable diff --git a/packageship/test/common_files/correct_test_result_json/build_depend.json b/packageship/test/common_files/correct_test_result_json/build_depend.json new file mode 100644 index 00000000..de32263b --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/build_depend.json @@ -0,0 +1,91 @@ +[ + { + "input": { + "sourceName": "A" + }, + "output": { + "code": "2001", + "data": { + "A_src": [ + "source", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "A2", + "install" + ] + ] + ], + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "B1", + "install" + ], + [ + "D1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "A1", + "install" + ], + [ + "C1", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ] + }, + "msg": "Successful Operation!" + } + } +] \ No newline at end of file diff --git a/packageship/test/common_files/database_file_info.yaml b/packageship/test/common_files/database_file_info.yaml new file mode 100644 index 00000000..01439675 --- /dev/null +++ b/packageship/test/common_files/database_file_info.yaml @@ -0,0 +1,6 @@ +- database_name: openEuler-20.03-LTS + priority: 1 + status: enable +- database_name: openEuler-20.04-LTS + priority: 2 + status: enable diff --git a/packageship/test/common_files/db_origin/data_1_bin.sqlite b/packageship/test/common_files/db_origin/data_1_bin.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8d5051866a938530ba81ffb7a1b971e4ef3c8f78 GIT binary patch literal 40960 zcmeI5U2Ggj9l&pI@4UC`Sd%D1R%I+kjw%{8KKssb(o#imUpA+>PH-I22pTiDH+Q#q zZ}+;pXV>&8g$mSnka$H%yz#~Z>RVKV%7@g#1Mtuy@gW6-1c)#3A;EukcJ3>-Q%fLO zUEbW?r00pZU5KnBOx$Yg)Oo@e2^*fdr5M5*;wD)uC3R$ zr1D+>dCE6#Y}87dv|3v$-QL)yYo(1_HNnXk?T23A?>SA`59-{Q05y^&A*FI2+7D9` z{h%_dcxpoZZd;`spLyzB2VH|#Ncm$iAQ4Be`)L(hpJBh!IGX>A)M)!IgF8@f!4d{qb%%eD3T zCWS+$)3vP|&(oIU@{oS=buRnym)RhzZ$kDD56lFpor~zE!VN5lUU|A zLf_@liCPo(0dRPtYPi>7o_(TP0&e@=F87)zX-?Sh^;{069${T=v-)XW-Z@!0gUMkc zra0C4nTb1Rm_yw;$iMV zXa`O&nwWh1fjgQja=Ls++;t1^c{MWk#_~qrae>ZQLeT5(z`ek|@d;dr_h3x8df)Lg#aQCrOwn z5+^wxROPySQeT`Xdd=CJWaj!d6S#Le0dI}kKS*dyeC*jBZtp_h?L-H?(a*!1TlMEl zTX*R7+8z4TLNc;0!fY(o=DF$V6s*j@b;dn&Vfwx4uTF1)01qU91dsp{Kmter2_OL^ z@c&JqmYYkFb8{acPYl|5b#V4fwaCOj=~pf<=L?19V!mY;pRF#6i$!^n4+A?7>x`Ol zjGC*}!uwQHnTpk%AF*b2V9k}vh&72Z6=BSTEtTvsN>(c)wj85mJrgUL9kJxufhCvA zLra$P#pOZ_rVO^g)uOyoWY+a#6s?qp);vnl^>nOgZp5N%1B;5KF^g6jiA^PG(@JB) zs@Go8J}CMfwAH`J)-##8)e9G|rPHgru74`^M4>ch*H9h3?Ku(MhV2!{Ytyv=_9OQF zU{^d!rIkvY=!BF}*J*d6eJ(;89)uC^###0QM-;q7S+nbS4#)#}={0H4_dKDrFKU1t zAJ`&sJX*2p5glwcRm5o8!iYsOktKgcc+)&#J30@XrIU|`>k!h9!+dLLO@vPECz-h&4 zdxt4Xz4;mK-+xP+4{j#gOiOW3!i-SJuPhb|d7l^Z$>byEsj$O}t5?JW{v3HHMgB?t zLVizvLw-r#B|iZH9!LNQAOR$R1dsp{Kmter2_OL^fCP}h=_QcWiS|rRgjWp_UYQc% zYF31oGa_8kMOaK{bYd3d`v0vI`3LzE`5pN+c@I|qZSt7Bwr(60U;hp00|%gB!C2v01`j~NB{{S0VIF~kidVLfMprygi5X2 zG%PbYFR`?hQ}Tvx8uHw%GHWUkR~i{BtIi>NCOI$LSyNB4mzkAL#3egxY3jV8i|79q z`L6K)|10?e`7L+={G9xZ{FwX@gm@qUB!C2v01`j~NB{{S0VIF~kN^@u0;h>UR@XGc z5@8m6{2_$L`Y9QKz z1dsp{Kmter2_OL^fCP{L5a8H$&`{*nIZFJm88P5Qj$@UvYn9}|k3fV65H%+qQSutVxc`6rQUG`fh%Dvt I*8tdm0HHW*d;kCd literal 0 HcmV?d00001 diff --git a/packageship/test/common_files/db_origin/data_1_src.sqlite b/packageship/test/common_files/db_origin/data_1_src.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..2b053b0a8f3166558bfcf63505421984902def50 GIT binary patch literal 28672 zcmeI4UuYyr8Nj=zdoq9AP#fYImNi(6EGBHGGfD3JvxneJrW41@W@9pvTiCl$(^E6k zZco=-bo-eMx zJeOPgH0UpA{ey?ybbeaTT~t3c;wjo;#0z{}n&^Mc&E}Stm(6dy6th-v=5(q(JJH^( z*7n?|dD?VeY_4g~I|1uPC3-I41L5_i1tTsaUxd>l$pg+JJ}pXpZ0tAPUH33iK9De` zcJJPvTivI1cc=Qby(ZnM?j5GgFxcXN3;|Lyfy`rR&4O|L zc0r?hl%yXhRd<>osk?h_6I7`;l1_ron!DTBr%;l9x^sB%F75aMkLdB2-9wiqzS1vJ z5wxA;=K0z2Db6cQ8Avhm+gGyUiLev+UVIhLL@N>jkFR7W8Uo~|M+$fhdOI+L#SBxF-@>g462mFPc@+hhwm zzi0A&!}y6%za*}sVZYBLY(D3jVTpUvAH*}8!cYd&qhi0$PgK;EU>}{@6STZF^aG%C zp+gS(ClCufytuI_@d(zGkki_gv@6AMFe}uWdm@TwJ0dZZ9*23e9X-z4T%JTRlQ8KC zmnPld!6luOS9QU)$bZC7CLJ?VbeA83sUYC9`gEsyYyN1KSs*;7aG&%f?@arD*3p{T zj=diD9>TjjiN}NKefr?AakqMSpT6MUrz@q&%36ipSe?!7kNv^8_PuKd*FFIaEJy$e zAOR$R1dsp{Kmter3H%QdxMSZm%4G2wa^n)`pPec7NO~1#W23U^biBh=~B}(E6!$RLtFjUE9NJS4YGKX++g2X*s$%!YWePO8Dz>GFqVhq&V)aFn9yX8YE+h)5=+J8D`H*d($4qvZJD9j>`f?!knw2K(jzVf<>PXj^U^ByBg$w;a=u2zfQRMeB)LXCZDTO8QIwnoWp%ox zQ7l=^yJNZnc2{Y4Vi?jX+VE*&*rAP(Mm-_pHR{LOO0nlhv5>w8=fiPXo$1;XMz2F& zB^3Q}qABjd2U-{)P9qA&$B>?*v7m5vhl3*(5?ZGczU3C<9;b=Iv3l-fNC0seQqLdx zf&WM!D5Ev_;_D3~KkQNwQZ-}D87OS=_>}XIK3A#GnzQW`S=iP$8dej=0k5xfPy5~T zyS-pcS-<7ODRl@+{ct3LkqsY6?>x98pAtDlui@@ zaPZTIr9w9=u-H5ffeKvw(1C`p5@tCbq*M8>EO%=)9xl{dE>XgD1`WrE%jvb*qu&%Ae0kNKyc z3G?xfL-X+)`GZL&b-l6h%RFOk z6rRYSZ6xRacMS3`@^|uA^3I3*fM^{FAOR$R1dsp{Kmter2_OL^fCP{L68Jy_vKBGx zwz_Ux>RQX#7AaKK@Bi~e8gTZ1i~N%Oocx4*pS%tNEJy$eAOR$R1dsp{Kmter2_OL^ zfCP}h2Ow}gpI=bZ&82$F*8ddcdhSfZCX%^SvfZ+hG1)T-n@H?bQu8v&80$>J5(&v9 zlEPBe(?f`PCTMAa`u?9MzcI+a$v?=u@7@<;M}5MV(9NB{{S0VIF~kN^@u0!RP} zAOR$R1U||HvX*Jud40{Pt2L*uS#{08F96_b*;WCrR-urb|KBvoKgr+7U*Por7Wp0d z6?yZc{D9~i5Bxa;n;H&FP9|RaNtB RT`@~lRWFmZ77EqYe*nTDuM7YH literal 0 HcmV?d00001 diff --git a/packageship/test/common_files/db_origin/data_2_bin.sqlite b/packageship/test/common_files/db_origin/data_2_bin.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..76533ab871181f22041d56ed879eb4f841e4fd35 GIT binary patch literal 40960 zcmeI5Uu+yl9mj8P@4UC`Sd%D1RuwEpuBtX7KKssb(ojXP_s8ZG*9ndTji51mdvkY- z_jZrnJ*TEmDO4cd;f)uB#2ars@LItGluAWBP$UXc0V({E0P#-(B>2tD&i#v>%2kXa z^|z9DXMZ#Eo!@+BcFsFHdw25_n_j|bJB)fxLRT}VGn$t97^Rs^CJ%oW{HcvVAg4~? zsEvh>3g$D#=l(JWv6k^lX70tg?5S^Ak@+L@bEZ|O81H})Hza@rkN^@u0`E%#cPwN6 zsRiwx;RP-BQp7$#@FErmPLHXuRo$-JJ9XOFs?}ehi^|NCEY4Ziw??K#`pld~>7t0D z_RZ>?b^eZN%s&bGrqJ*2byR+JG$-Z9QXHWUDXy`D#Z>=Zy`bJ}m z!Y1SC`u6qbY1{KzOh5JU`gWa4Ui2yr{Z>()JU5+Qb=zZPQ7Dg*#SMeD@43lwJPVs~ z=(FTlwTTA+I6hV}Ea*Ccd#q9-=7zl<3tGpiPCV%MeFnMS#k=}e?K8T(a`JKnlih?5 zajNliQ&-L?g}QRmt~J@)<#lsAYs`NL#z%czz0p$I`k~s5))C6ktspKP8egTFxFo-N zR$h{+7fL#-db%7Mm03BdF7L@9OzMv=R5{d-&YkF8hsy_s)I-M4c+l%P5e)JpWf%?J z$m=IlgYO{n$5SOQS@Zw@>k=uAXJ{oXEI3oMu%z`1!J`cxQ=t8;57 z3J3jZqfqXKaWWkdhl9vvkZ(Go%bXUAcH_i}AkzabrP1KQA(3fQO}G|&Utqgw#8eg$ z9zW8`pjb5v(Ce4%&F?mGym=>|J2j7@63K<_LH*&Ot>KdB!C2v01`j~NB{{S zfybJFZJx`JGv^3-V(8}DFmz?Td9!j+s8qQ#OENqZ$v;G7tvd3hoYshol&82 zeXr9^?lQio@j;xho=9>x@_5Dz)M@p+zyo_EPlFbX20_3@2fPHB@qsB4FQ8Sck1_+D~A?|MlWlC*vHk_S_?bl-_Q2KKN`2QiB* zFM!h94$S%#Eom3~0+q9LjHj<05SYfVc&)!{7qshUsI+r!BADhN)QOmt7S|H0 zsvTq#aBm`*JAq=MBye!xCvmZxB>j&pFT1?0igL06dO(L21C}h`Ir!-2orBqzH#cr< z?w3EDrVaZnU;~&cFG9jwLELAq*Y;QoX84wP7h=)U%F4x~`s>;I=lh2VWxoGe?Z5xb zn)h#{(@e{vCE-FY@nNo13|Xm|j$eGJi@Q#F?Gk^$Z<5zD+p^2H2%_ut#( zby)lVZSplZ#SIA{0VIF~kN^@u0!RP}AOR$R1dsp{c(epg8wI)8f4efmLEg%c|A_VfzbC(eZvcGj z(N+-EkN^@u0!RP}AOR$R1dsp{Kmter2_S)!A;6dX7vKi~;DrGA0RVU*K)ec|!w&#x zT2{UVkkxh5l=uH{PJRFX$@qk-NB{{S0VIF~kN^@u0!RP}AOR$R1dza^At2rcXp7eX YYU(urvEpC71|VMwfb{_4`~Thl0gN|nZU6uP literal 0 HcmV?d00001 diff --git a/packageship/test/common_files/db_origin/data_2_src.sqlite b/packageship/test/common_files/db_origin/data_2_src.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..e4bbb155d066e6ee52f5119563f93fc15cfd7a16 GIT binary patch literal 28672 zcmeI4&2Qtz6~IMNAC}ez*I2a1VryieVo|%1Xj$41q(On`gLc(;y{@goSzr@)M2#e7 zDULd%qh1=U4rM~#*I z<|~=(+E>ATPT3zl>c`{bYv#Q98I239gT{+&T8QkwZl$wp>+9MZF9)>aK6N^_o{p@y zYxOB ztIt|*b>kuWLfftG1(so1L0=TIKOmY4*`HifAsd^FugQ3&hanq_V<#7fR%rh$Zo@6; ztWCwohVh3&eL-CN}{?6O_C&c3hxy zu0xE5ClCwFJHN3=@dVZs7qix-v@iI0G%r+^2iyT*1o%5~YiJ=H1SnlqVa=5m`#+$RIUdb9pN?dZ&H z$M%5PkKo;%1k=&%K7DZ5x>q}VKwh*Skc~pLvNmBiR=N3=pC*=nvCNkn%U{0ogOy}*GnGmmC$4YTBK=CW6OCvH?)#+!mj?2S zCOK>$kzFVh5Z)s-&vu~NK#r;CQNhZjW;RW-0o5CikD=nxC56XKaKfggE@OpF;`oG; zo?xs<_=tI>Fq142Tg@?w=Ew_1-IC01i600Wu>O>6z`UEJA59G96gBZ_II&4u9`OfU z1V!QmYL@)K@dGX#8}h?xN#=B20@_Sn{tQ?IKU3RWBRfY+2)YD%PPuqgBmmdz7Kxk=pu6lf2Wp(EIpU{G^u{g$e*-=& z$xJ`UE4a+-J03IhdFhyv45)ktcmT(PNhZwYBUxf1ohdnn(nAkQzG^}EWaD^ecM~Qg zs+=G}f$L*0U`!PA1K#OO;hls=BSQcj{B&TckV+ZW+aL%=-`Luc@1jh*Ce*+l-u&Q? z4{yHn%iV*$N#$APef_AZuJ>APb#2w}?Jy{6P;wB0tPRJ0SO(jYE7ezpjL8s~4JUU? zm8uChW={=?(3@QJSAYJs56;`s-u*4tp8VR=p1iem5LHsQ%3I)Bsce=_2$52_XP6K) zCe%ax0j=D=lZP!PYy2r;{MY!G@ekuYKcU1mwsr=tk`F~U8{|%M@*H!*slfVDZ7(YwExBs_|KN!C=egh3W zkN^@u0!RP}AOR$R1dsp{Kmter2_S(_fxy+w(yE+BTWfYw>OW4ontm#wkt7vM8dMK; z^jK1-NhL9op(M9fvqORQRM1fZ`Taj*{3T)h&-l0TzVSEX9cbWz1dsp{Kmter2_OL^ zfCP{L5%>0ibC~J*DU1tmkrJ{ty5D|3Ah*jlVX|IgD4MqiNt5@5w LPNwzMT&?qepkSk6 literal 0 HcmV?d00001 diff --git a/packageship/test/common_files/dbs/maintenance.information.db b/packageship/test/common_files/dbs/maintenance.information.db new file mode 100644 index 0000000000000000000000000000000000000000..d43b5e4e10a2b922a2931664afe5cb6aba22852f GIT binary patch literal 8192 zcmeI#F-rq66bJC66r=}IH%H&#a0gZ@;%+QKi|487DRfbyeTBeX4$h;q_LDcK)}b9- zMgEV3@ZL)@{I)l_PhG<_ujq~7YyUi(kNYf*Vdft{ykIIa$l!?!V!=x=PxPEh$->QH9 zYrXSa|MX72-+c9)FL=SlR``v+U}@??HI~@Be)- z?f2v5@IrQPcamvrzcI@8#nj#+7YL=~7276+RNyMXRSW}!yox~!@egx_)L;Ig3I)cp zNoqgW*z)%va3BFBfCP{L5eZ|CcyB7gVLTRt`}fA#=rD=m z$wE+S1-{ew?Y`r7e0w2fuQMx&>~^p3Z~KAWyVbXQcRHPGHnUQ(AKQ1Gp!KQ~tlwxf zHl(>0CTZKdA3s1fI6+nwwUjS9O=Vf#lXOm=<&h|pH*bB3^&b1e2tNtDa z?1ks3YV4t@8Y@s0v$oJ!;XbX#hRGmKCueN_$><;(#CL+u+G!Zk=`N2Rw&Q^EBr;mf9JpQ zANjZZEB+b(n19IMP4)=F3hyXc&^=nWw6>zYpg0YCE#mpIrp7GMYEQqwg_m(@{(3)>deZu7nTjH za#9kOEK^EiIl^FNC233OC05F-LF>%OeNSj6({mrRVQHW(LS3O-R7&_SLX+oz#vc>@ z2YdnW3%mjNj(^R+gm(a+fQbVMAOR$R1dsp{Kmter2_OL^fCP{L68KjMn9P8KzZaRN tC51aEnH4D<5T+EZ&@juYCe+Q6N`WwxfHwiUsu5aQ17UfhOy%r~KLHise-;1$ literal 0 HcmV?d00001 diff --git a/packageship/test/common_files/dbs/openEuler-20.04-LTS.db b/packageship/test/common_files/dbs/openEuler-20.04-LTS.db new file mode 100644 index 0000000000000000000000000000000000000000..1a8981105b6c6d0dd13db4365af338521a3f0d95 GIT binary patch literal 20480 zcmeI2Ply{;9LL|AKbg#AlV6uo7A(cbUb>4VO|tE!J=94uy9SeVolOgSS|*t(j?GTe zNp__dWtZYj@an;vHwCYvcoIE%Q4sMWf(OBq7r~>yH`$pGcNJC(3jIA8-uL_az2A`E zhnK^9%+4$Aa2kjQqj=w&iVb#wan4>8f-#oIN5>}}8VdQOgQwze<~%Fk`M!t?v?YU; zekxf@-=UyE1c(3;AOb{y2oM1xKm>@ue@Wn|qZun#ukz#Fsn;I{V{h-cFz$#yWV!hLQ1SzX6TDnx94m*uIS$FiSE62`rOE0uK_ zTufr%(``6A0^hYK ze@#+jtshs^q@O3x&-;mkM+aei(obr%)BU8WbNAy18vh*N~7mxbQr{wa1>>Gp_cB2tgUnHg{v-mzIN>S!(|#;KHVlr*%L65FhltF@wy>h)ykT<;Ty9k{WXnKpqrbJ?KCHbUyvP^zon|t3TD@d$)jO|Fr{i1KuaAe`>Y5g-CYfCvx)B0vO)01+SpPnAI4 z)Y&qy&1$ufM|FucYk1zTE*aABW>p%kkVAKoH)KjJ=re<ewNq~7YyUi(kNYf*Vdft{ykIIa$l!?!V!=x=PxPEh$->QH9 zYrXSa|MX72-+c9)FL=SlR``v+U}@??HI~@Be)- z?f2v5@IrQPcamvrzcI@8#nj#+7YL=~7276+RNyMXRSW}!yox~!@egx_)L;Ig3I)cp zNoqgW*z)%va3BFBfCP{L5eZ|CcyB7gVLTRt`}fA#=rD=m z$wE+S1-{ew?Y`r7e0w2fuQMx&>~^p3Z~KAWyVbXQcRHPGHnUQ(AKQ1Gp!KQ~tlwxf zHl(>0CTZKdA3s1fI6+nwwUjS9O=Vf#lXOm=<&h|pH*bB3^&b1e2tNtDa z?1ks3YV4t@8Y@s0v$oJ!;XbX#hRGmKCueN_$><;(#CL+u+G!Zk=`N2Rw&Q^EBr;mf9JpQ zANjZZEB+b(n19IMP4)=F3hyXc&^=nWw6>zYpg0YCE#mpIrp7GMYEQqwg_m(@{(3)>deZu7nTjH za#9kOEK^EiIl^FNC233OC05F-LF>%OeNSj6({mrRVQHW(LS3O-R7&_SLX+oz#vc>@ z2YdnW3%mjNj(^R+gm(a+fQbVMAOR$R1dsp{Kmter2_OL^fCP{L68KjMn9P8KzZaRN tC51aEnH4D<5T+EZ&@juYCe+Q6N`WwxfHwiUsu5aQ17UfhOy%r~KLHise-;1$ literal 0 HcmV?d00001 diff --git a/packageship/test/common_files/operate_dbs/openEuler-20.04-LTS.db b/packageship/test/common_files/operate_dbs/openEuler-20.04-LTS.db new file mode 100644 index 0000000000000000000000000000000000000000..1a8981105b6c6d0dd13db4365af338521a3f0d95 GIT binary patch literal 20480 zcmeI2Ply{;9LL|AKbg#AlV6uo7A(cbUb>4VO|tE!J=94uy9SeVolOgSS|*t(j?GTe zNp__dWtZYj@an;vHwCYvcoIE%Q4sMWf(OBq7r~>yH`$pGcNJC(3jIA8-uL_az2A`E zhnK^9%+4$Aa2kjQqj=w&iVb#wan4>8f-#oIN5>}}8VdQOgQwze<~%Fk`M!t?v?YU; zekxf@-=UyE1c(3;AOb{y2oM1xKm>@ue@Wn|qZun#ukz#Fsn;I{V{h-cFz$#yWV!hLQ1SzX6TDnx94m*uIS$FiSE62`rOE0uK_ zTufr%(``6A0^hYK ze@#+jtshs^q@O3x&-;mkM+aei(obr%)BU8WbNAy18vh*N~7mxbQr{wa1>>Gp_cB2tgUnHg{v-mzIN>S!(|#;KHVlr*%L65FhltF@wy>h)ykT<;Ty9k{WXnKpqrbJ?KCHbUyvP^zon|t3TD@d$)jO|Fr{i1KuaAe`>Y5g-CYfCvx)B0vO)01+SpPnAI4 z)Y&qy&1$ufM|FucYk1zTE*aABW>p%kkVAKoH)KjJ=re<ewN Date: Wed, 1 Jul 2020 21:46:35 +0800 Subject: [PATCH 47/72] solve the duplicate result problem --- .../application/apps/package/function/build_depend.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py index 38b2a45f..e3bdac13 100644 --- a/packageship/packageship/application/apps/package/function/build_depend.py +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -168,7 +168,10 @@ class BuildDepend: if not obj.bin_name: continue - + # for first loop, init the source_dict + if not self.source_dict: + for src_name in pkg_name_li: + self.source_dict[src_name] = [obj.db_name, obj.search_version] if obj.bin_name not in self.result_dict: self.result_dict[obj.bin_name] = [ obj.source_name if obj.source_name else None, @@ -185,7 +188,8 @@ class BuildDepend: node_list.append(node) if obj.source_name and \ - obj.source_name not in self.source_dict: + obj.source_name not in self.source_dict and \ + obj.source_name not in self.history_dicts: self.source_dict[obj.source_name] = [obj.db_name, obj.version] next_src_set.add(obj.source_name) -- Gitee From 40c89f47b3766cf374b87eec2d38f457edf999de Mon Sep 17 00:00:00 2001 From: gongzt Date: Thu, 2 Jul 2020 08:50:40 +0800 Subject: [PATCH 48/72] Code comment normalization modification, sqlhelper value exception handling --- .../application/initsystem/data_import.py | 257 ++++++++++--- .../application/initsystem/datamerge.py | 59 ++- .../libs/dbutils/sqlalchemy_helper.py | 2 +- packageship/packageship/pkgship.py | 338 +++++++++++++++--- 4 files changed, 551 insertions(+), 105 deletions(-) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index e7dafde2..d4aa379e 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -28,14 +28,16 @@ LOGGER = Log(__name__) class InitDataBase(): ''' - Database initialization, generate multiple databases and data based on configuration files + Database initialization, generate multiple databases and data based on configuration files + Attributes: + config_file_path: configuration file path + config_file_datas: initialize the configuration content of the database + db_type: type of database ''' def __init__(self, config_file_path=None): ''' - parameter: - config_file_path:The path of the configuration file that needs to initialize - the database in the project + Class instance initialization ''' self.config_file_path = config_file_path @@ -57,9 +59,15 @@ class InitDataBase(): def __read_config_file(self): ''' - functional description: - Read the contents of the configuration file load each node data in the yaml + Read the contents of the configuration file load each node data in the yaml configuration file as a list to return + Args: + + Returns: + Initialize the contents of the database configuration file + Raises: + FileNotFoundError: The specified file does not exist + TypeError: Wrong type of data ''' if not os.path.exists(self.config_file_path): @@ -81,7 +89,13 @@ class InitDataBase(): def init_data(self): ''' - functional description:Initialization of the database + Initialization of the database + Args: + + Returns: + + Raises: + IOError: An error occurred while deleting the database information file ''' if getattr(self, 'config_file_datas', None) is None or \ self.config_file_datas is None: @@ -118,7 +132,14 @@ class InitDataBase(): def _create_database(self, database): ''' create related databases + Args: + database: Initialize the configuration content of the database + Returns: + The generated mysql database or sqlite database + Raises: + SQLAlchemyError: Abnormal database operation ''' + db_name = database.get('dbname') self._sqlite_db = SqliteDatabaseOperations(db_name=db_name) @@ -136,10 +157,19 @@ class InitDataBase(): def _init_data(self, database): ''' - functional description:data initialization operation - parameter: - database:database configuration information + data initialization operation + Args: + database: Initialize the configuration content of the database + Returns: + + Raises: + ContentNoneException: Exception with empty content + TypeError: Data type error + SQLAlchemyError: Abnormal database operation + DataMergeException: Error in data integration + IOError: An error occurred while deleting the database information file ''' + try: db_file = None # 1. create a database and related tables in the database @@ -193,8 +223,15 @@ class InitDataBase(): def _src_package_relation(self, src_package_data): ''' - Mapping of data relations of source packages + Mapping of data relations of source packages + Args: + src_package_data: Source package data + Returns: + + Raises: + ''' + _src_package_name = src_package_data.name _src_package = { "name": src_package_data.name, @@ -220,8 +257,15 @@ class InitDataBase(): def _src_requires_relation(self, src_requires_data): ''' - Source package dependent package data relationship mapping + Source package dependent package data relationship mapping + Args: + src_requires_data: Source package dependent package data + Returns: + + Raises: + ''' + _src_package_name = self._src_package_names.get( src_requires_data.pkgKey) if _src_package_name: @@ -233,8 +277,15 @@ class InitDataBase(): def _bin_package_relation(self, bin_package_data): ''' - Binary package relationship mapping problem + Binary package relationship mapping problem + Args: + bin_package_data: Binary package data + Returns: + + Raises: + ''' + _bin_pkg_key = bin_package_data.pkgKey self._bin_package_name[bin_package_data.name] = _bin_pkg_key @@ -258,8 +309,15 @@ class InitDataBase(): def _bin_requires_relation(self, bin_requires_data): ''' - Binary package dependency package relationship mapping problem + Binary package dependency package relationship mapping problem + Args: + bin_requires_data: Binary package dependency package data + Returns: + + Raises: + ''' + _bin_pkg_key = bin_requires_data.pkgKey if _bin_pkg_key: if _bin_pkg_key not in self._bin_requires_dicts: @@ -271,7 +329,13 @@ class InitDataBase(): def _bin_provides_relation(self, bin_provides_data): ''' - Binary package provided by the relationship mapping problem + Binary package provided by the relationship mapping problem + Args: + bin_provides_data: Component data provided by the binary package + Returns: + + Raises: + ''' _bin_pkg_key = bin_provides_data.pkgKey if _bin_pkg_key: @@ -284,7 +348,14 @@ class InitDataBase(): def data_relationship(self, db_file): ''' dependencies between combined data + Args: + db_file: Temporary database file + Returns: + + Raises: + ''' + self._bin_provides_dicts = dict() self._bin_requires_dicts = dict() self._bin_package_name = dict() @@ -321,7 +392,14 @@ class InitDataBase(): def file_merge(self, src_package_paths, bin_package_paths): ''' - integration of multiple data files + integration of multiple data files + Args: + src_package_paths: Source package database file + bin_package_paths: Binary package database file + Returns: + Path of the generated temporary database file + Raises: + DataMergeException: Abnormal data integration ''' _db_file = os.path.join( self._sqlite_db.database_file_folder, 'temporary_database') @@ -362,13 +440,14 @@ class InitDataBase(): def __exists_repeat_database(self): ''' - functional description:Determine if the same database name exists - parameter: - return value: - exception description: - modify record: - ''' + Determine if the same database name exists + Args: + + Returns: + True if there are duplicate databases, false otherwise + Raises: + ''' db_names = [name.get('dbname') for name in self.config_file_datas] @@ -379,7 +458,13 @@ class InitDataBase(): def _save_bin_package(self, src_packages): ''' - Save binary package data + Save binary package data + Args: + src_packages: Source package data + Returns: + + Raises: + ''' bin_packages = [] for package_data in src_packages: @@ -429,7 +514,13 @@ class InitDataBase(): def _save_bin_provides(self, bin_packages): ''' - Save package data provided by binary + Save package data provided by binary + Args: + bin_packages: Binary package data + Returns: + + Raises: + ''' bin_provides_list = [] for bin_pack_entity in bin_packages: @@ -460,8 +551,15 @@ class InitDataBase(): def save_data(self, db_name): ''' - save related package data + save related package data + Args: + db_name: The name of the database + Returns: + + Raises: + ''' + with DBHelper(db_name=db_name) as data_base: # Add source package data data_base.batch_add( @@ -491,11 +589,16 @@ class InitDataBase(): @staticmethod def __updata_settings_file(**Kwargs): ''' - update some configuration files related to the database in the system - parameter: - **Kwargs:data related to configuration file nodes - database_name: Name database - priority: priority + update some configuration files related to the database in the system + Args: + + Returns: + **Kwargs:data related to configuration file nodes + database_name: Name database + priority: priority + Raises: + FileNotFoundError: The specified file was not found + IOError: File or network operation io abnormal ''' try: if not os.path.exists(DATABASE_FILE_INFO): @@ -516,12 +619,15 @@ class InitDataBase(): @staticmethod def delete_settings_file(): ''' - functional description:Delete the configuration file of the database - parameter: - return value: - exception description: - modify record: + Delete the configuration file of the database + Args: + + Returns: + True if the deletion is successful, otherwise false + Raises: + IOError: File or network operation io abnormal ''' + try: if os.path.exists(DATABASE_FILE_INFO): os.remove(DATABASE_FILE_INFO) @@ -533,7 +639,13 @@ class InitDataBase(): def delete_db(self, db_name): ''' - Delete the database + Delete the database + Args: + db_name: The name of the database + Returns: + + Raises: + IOError: File or network operation io abnormal ''' if self.db_type == 'mysql': del_result = MysqlDatabaseOperations.drop_database(db_name) @@ -561,7 +673,11 @@ class InitDataBase(): class MysqlDatabaseOperations(): ''' - Related to database operations, creating databases, creating tables + Related to database operations, creating databases, creating tables + Attributes: + db_name: The name of the database + create_database_sql: SQL statement to create a database + drop_database_sql: Delete the SQL statement of the database ''' def __init__(self, db_name): @@ -574,7 +690,13 @@ class MysqlDatabaseOperations(): def create_database(self): ''' - functional description:create a database + create a database + Args: + + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database ''' with DBHelper(db_name='mysql') as data_base: @@ -593,12 +715,13 @@ class MysqlDatabaseOperations(): @classmethod def drop_database(cls, db_name): ''' - functional description:Delete the database according to the specified name - parameter: - :db_name:The name of the database - return value: - exception description: - modify record: + Delete the database according to the specified name + Args: + db_name: The name of the database to be deleted + Returns: + + Raises: + SQLAlchemyError: An exception occurred while creating the database ''' if db_name is None: raise IOError( @@ -632,7 +755,13 @@ class MysqlDatabaseOperations(): def create_datum_database(self): ''' - Create a benchmark database to save the maintainer's information + Create a benchmark database to save the maintainer's information + Args: + + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database ''' with DBHelper(db_name='mysql') as data_base: # create database @@ -666,7 +795,10 @@ class MysqlDatabaseOperations(): class SqliteDatabaseOperations(): ''' - sqlite database related operations + sqlite database related operations + Attributes: + db_name: Name database + database_file_folder: Database folder path ''' def __init__(self, db_name, **kwargs): @@ -674,11 +806,11 @@ class SqliteDatabaseOperations(): self.db_name = db_name self._read_config = ReadConfig() if getattr(kwargs, 'database_path', None) is None: - self.database_file_path() + self._database_file_path() else: self.database_file_folder = kwargs.get('database_path') - def database_file_path(self): + def _database_file_path(self): ''' Database file path ''' @@ -696,7 +828,15 @@ class SqliteDatabaseOperations(): def create_sqlite_database(self): ''' - create sqlite database and table + create sqlite database and table + Args: + + Returns: + After successful generation, return the database file address, + otherwise return none + Raises: + FileNotFoundError: The specified folder path does not exist + SQLAlchemyError: An error occurred while generating the database ''' if self.database_file_folder is None: raise FileNotFoundError('Database folder does not exist') @@ -721,8 +861,15 @@ class SqliteDatabaseOperations(): def drop_database(self): ''' - Delete the specified sqlite database + Delete the specified sqlite database + Args: + + Returns: + Returns true after successful deletion, otherwise returns false + Raises: + IOError: An io exception occurred while deleting the specified database file ''' + try: db_path = os.path.join( self.database_file_folder, self.db_name+'.db') @@ -736,7 +883,15 @@ class SqliteDatabaseOperations(): def create_datum_database(self): ''' - create sqlite database and table + create sqlite database and table + Args: + + Returns: + After successful generation, return the database file address, + otherwise return none + Raises: + FileNotFoundError: The specified database folder does not exist + SQLAlchemyError: An error occurred while generating the database ''' if self.database_file_folder is None: raise FileNotFoundError('Database folder does not exist') diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index cea7512a..7691dbee 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -17,7 +17,11 @@ LOGGER = Log(__name__) class MergeData(): ''' - Load data from sqlite database + Load data from sqlite database + Attributes: + db_file: Database file + db_type: Connected database type + datum_database: Base database name ''' def __init__(self, db_file): @@ -36,7 +40,13 @@ class MergeData(): def get_package_data(self): ''' - get binary package or source package data + get binary package or source package data + Args: + + Returns: + All source package data queried + Raises: + SQLAlchemyError: An error occurred while executing the sql statement ''' try: with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ @@ -52,7 +62,13 @@ class MergeData(): def get_requires_data(self): ''' - get dependent package data of binary package or source package + get dependent package data of binary package or source package + Args: + + Returns: + All dependent data queried + Raises: + SQLAlchemyError: An error occurred while executing the sql statement ''' try: with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ @@ -67,7 +83,13 @@ class MergeData(): def get_provides(self): ''' - get the dependency package provided by the binary package + get the dependency package provided by the binary package + Args: + + Returns: + Query the component data provided by all binary packages + Raises: + SQLAlchemyError: An error occurred while executing the sql statement ''' try: with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ @@ -82,7 +104,13 @@ class MergeData(): def get_maintenance_info(self): ''' - Obtain the information of the maintainer + Obtain the information of the maintainer + Args: + + Returns: + Maintainer related information + Raises: + SQLAlchemyError: An error occurred while executing the sql statement ''' try: if not hasattr(self, 'mainter_infos'): @@ -100,7 +128,15 @@ class MergeData(): def src_file_merge(self, src_package_key, db_file): ''' - Source code related data integration + Source code related data integration + Args: + src_package_key: The relevant key value of the source package + db_file: Database file + Returns: + Key value after successful data combination + (0, False) or (src_package_key, True) + Raises: + SQLAlchemyError: An error occurred while executing the sql statement ''' self.get_maintenance_info() @@ -240,9 +276,16 @@ class MergeData(): def bin_file_merge(self, bin_package_key, db_file): ''' - Binary package related data integration + Binary package related data integration + Args: + bin_package_key: Primary key of binary package + db_file: Database file + Returns: + Key value after successful data combination + (0, False) or (bin_package_key, True) + Raises: + SQLAlchemyError: An error occurred while executing the sql statement ''' - self.__compose_bin_package() # binary package dependent package integration diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index ae42ad8b..0ac3eac0 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -109,7 +109,7 @@ class DBHelper(): ''' session = sessionmaker() - if getattr(self, 'engine') is None: + if not hasattr(self, 'engine'): raise DisconnectionError('Abnormal database connection') session.configure(bind=self.engine) diff --git a/packageship/packageship/pkgship.py b/packageship/packageship/pkgship.py index e5e8534a..199c9a46 100644 --- a/packageship/packageship/pkgship.py +++ b/packageship/packageship/pkgship.py @@ -27,7 +27,13 @@ DB_NAME = 0 def main(): ''' - command entry function + Command line tool entry, register related commands + Args: + + Returns: + + Raises: + Error: An error occurred while executing the command ''' try: packship_cmd = PkgshipCommand() @@ -39,7 +45,11 @@ def main(): class BaseCommand(): ''' - Basic attributes used for command invocation + Basic attributes used for command invocation + Attributes: + write_host: Can write operation single host address + read_host: Can read the host address of the operation + headers: Send HTTP request header information ''' def __init__(self): @@ -80,7 +90,12 @@ class BaseCommand(): class PkgshipCommand(BaseCommand): ''' - PKG package command line + PKG package command line + Attributes: + statistics: Summarized data table + table: Output table + columns: Calculate the width of the terminal dynamically + params: Command parameters ''' parser = argparse.ArgumentParser( description='package related dependency management') @@ -100,13 +115,25 @@ class PkgshipCommand(BaseCommand): @staticmethod def register_command(command): ''' - Register command + Registration of related commands + Args: + + Returns: + + Raises: + ''' command.register() def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' for command_params in self.params: self.parse.add_argument( # pylint: disable=E1101 @@ -118,7 +145,13 @@ class PkgshipCommand(BaseCommand): @classmethod def parser_args(cls): ''' - Command parsing + Register the command line and parse related commands + Args: + + Returns: + + Raises: + Error: An error occurred during command parsing ''' cls.register_command(RemoveCommand()) cls.register_command(InitDatabaseCommand()) @@ -138,7 +171,13 @@ class PkgshipCommand(BaseCommand): def parse_package(self, response_data): ''' - Parse the corresponding data of the package + Parse the corresponding data of the package + Args: + response_data: http request response content + Returns: + + Raises: + ''' if response_data.get('code') == ResponseCode.SUCCESS: package_all = response_data.get('data') @@ -152,7 +191,13 @@ class PkgshipCommand(BaseCommand): def parse_depend_package(self, response_data): ''' - Parse the corresponding data of the package + Parsing package data with dependencies + Args: + response_data: http request response content + Returns: + + Raises: + ''' bin_package_count = 0 src_package_count = 0 @@ -199,7 +244,15 @@ class PkgshipCommand(BaseCommand): def print_(self, content=None, character='=', dividing_line=False): ''' - Output formatted characters + Output formatted characters + Args: + content: Output content + character: Output separator content + dividing_line: Whether to show the separator + Returns: + + Raises: + ''' # Get the current width of the console @@ -213,7 +266,13 @@ class PkgshipCommand(BaseCommand): @staticmethod def create_table(title): ''' - Create printed forms + Create printed forms + Args: + + Returns: + + Raises: + ''' table = PrettyTable(title) # table.set_style(prettytable.PLAIN_COLUMNS) @@ -226,7 +285,14 @@ class PkgshipCommand(BaseCommand): def statistics_table(self, bin_package_count, src_package_count): ''' - Generate data for total statistical tables + Generate data for total statistical tables + Args: + bin_package_count: Number of binary packages + src_package_count: Number of source packages + Returns: + Summarized data table + Raises: + ''' statistics_table = self.create_table(['', 'binary', 'source']) statistics_table.add_row( @@ -242,6 +308,12 @@ class PkgshipCommand(BaseCommand): def http_error(response): ''' Log error messages for http + Args: + response: Response content of http request + Returns: + + Raises: + HTTPError: http request error ''' try: print(response.raise_for_status()) @@ -253,7 +325,10 @@ class PkgshipCommand(BaseCommand): class RemoveCommand(PkgshipCommand): ''' - Delete database command + Delete database command + Attributes: + parse: Command line parsing example + params: Command line parameters ''' def __init__(self): @@ -264,14 +339,27 @@ class RemoveCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(RemoveCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnErr: Request connection error + ''' if params.db is None: print('No database specified for deletion') @@ -297,7 +385,10 @@ class RemoveCommand(PkgshipCommand): class InitDatabaseCommand(PkgshipCommand): ''' - Initialize database command + Initialize database command + Attributes: + parse: Command line parsing example + params: Command line parameters ''' def __init__(self): @@ -309,14 +400,26 @@ class InitDatabaseCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(InitDatabaseCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ''' file_path = params.filepath try: @@ -340,7 +443,10 @@ class InitDatabaseCommand(PkgshipCommand): class UpdateDatabaseCommand(PkgshipCommand): ''' - update database command + update database command + Attributes: + parse: Command line parsing example + params: Command line parameters ''' def __init__(self): @@ -352,21 +458,37 @@ class UpdateDatabaseCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(UpdateDatabaseCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + + Returns: + + Raises: + ''' pass # pylint: disable= W0107 class AllPackageCommand(PkgshipCommand): ''' - get all package commands + get all package commands + Attributes: + parse: Command line parsing example + params: Command line parameters + table: Output table ''' def __init__(self): @@ -380,14 +502,26 @@ class AllPackageCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(AllPackageCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + + Returns: + + Raises: + ConnectionError: Request connection error ''' _url = self.read_host + \ '/packages?dbName={dbName}'.format(dbName=params.db) @@ -408,7 +542,10 @@ class AllPackageCommand(PkgshipCommand): class UpdatePackageCommand(PkgshipCommand): ''' - update package data + update package data + Attributes: + parse: Command line parsing example + params: Command line parameters ''' def __init__(self): @@ -425,14 +562,26 @@ class UpdatePackageCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(UpdatePackageCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + + Returns: + + Raises: + ConnectionError: Request connection error ''' _url = self.write_host + '/packages/findByPackName' try: @@ -440,7 +589,7 @@ class UpdatePackageCommand(PkgshipCommand): _url, data=json.dumps({'sourceName': params.packagename, 'dbName': params.db, 'maintainer': params.m, - 'maintainLevel': params.l}), + 'maintainlevel': params.l}), headers=self.headers) except ConnectionError as conn_error: LOGGER.logger.error(conn_error) @@ -459,7 +608,12 @@ class UpdatePackageCommand(PkgshipCommand): class BuildDepCommand(PkgshipCommand): ''' - query the compilation dependencies of the specified package + query the compilation dependencies of the specified package + Attributes: + parse: Command line parsing example + params: Command line parameters + collection: Is there a collection parameter + collection_params: Command line collection parameters ''' def __init__(self): @@ -477,7 +631,13 @@ class BuildDepCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(BuildDepCommand, self).register() # collection parameters @@ -489,7 +649,13 @@ class BuildDepCommand(PkgshipCommand): def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + + Returns: + + Raises: + ConnectionError: Request connection error ''' _url = self.read_host + '/packages/findBuildDepend' try: @@ -516,7 +682,12 @@ class BuildDepCommand(PkgshipCommand): class InstallDepCommand(PkgshipCommand): ''' - query the installation dependencies of the specified package + query the installation dependencies of the specified package + Attributes: + parse: Command line parsing example + params: Command line parameters + collection: Is there a collection parameter + collection_params: Command line collection parameters ''' def __init__(self): @@ -534,7 +705,13 @@ class InstallDepCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(InstallDepCommand, self).register() # collection parameters @@ -546,7 +723,13 @@ class InstallDepCommand(PkgshipCommand): def parse_package(self, response_data): ''' - Parse the corresponding data of the package + Parse the corresponding data of the package + Args: + response_data: http response data + Returns: + + Raises: + ''' if getattr(self, 'statistics'): setattr(self, 'statistics', dict()) @@ -594,7 +777,13 @@ class InstallDepCommand(PkgshipCommand): def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnectionError: requests connection error ''' _url = self.read_host + '/packages/findInstallDepend' try: @@ -622,7 +811,12 @@ class InstallDepCommand(PkgshipCommand): class SelfBuildCommand(PkgshipCommand): ''' - self-compiled dependency query + self-compiled dependency query + Attributes: + parse: Command line parsing example + params: Command line parameters + collection: Is there a collection parameter + collection_params: Command line collection parameters ''' def __init__(self): @@ -648,7 +842,13 @@ class SelfBuildCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(SelfBuildCommand, self).register() # collection parameters @@ -717,7 +917,13 @@ class SelfBuildCommand(PkgshipCommand): def parse_package(self, response_data): ''' - Parse the corresponding data of the package + Parse the corresponding data of the package + Args: + response_data: http response data + Returns: + Summarized data table + Raises: + ''' if getattr(self, 'statistics'): setattr(self, 'statistics', dict()) @@ -745,7 +951,13 @@ class SelfBuildCommand(PkgshipCommand): def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + params: commands lines params + Returns: + + Raises: + ConnectionError: requests connection error ''' _url = self.read_host + '/packages/findSelfDepend' try: @@ -779,7 +991,10 @@ class SelfBuildCommand(PkgshipCommand): class BeDependCommand(PkgshipCommand): ''' - dependent query + dependent query + Attributes: + parse: Command line parsing example + params: Command line parameters ''' def __init__(self): @@ -795,14 +1010,26 @@ class BeDependCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(BeDependCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + params: command lines params + Returns: + + Raises: + ConnectionError: requests connection error ''' _url = self.read_host + '/packages/findBeDepend' try: @@ -832,7 +1059,10 @@ class BeDependCommand(PkgshipCommand): class SingleCommand(PkgshipCommand): ''' - query single package information + query single package information + Attributes: + parse: Command line parsing example + params: Command line parameters ''' def __init__(self): @@ -847,14 +1077,26 @@ class SingleCommand(PkgshipCommand): def register(self): ''' - Command line parameter injection + Command line parameter injection + Args: + + Returns: + + Raises: + ''' super(SingleCommand, self).register() self.parse.set_defaults(func=self.do_command) def parse_package(self, response_data): ''' - Parse the corresponding data of the package + Parse the corresponding data of the package + Args: + response_data: http response data + Returns: + + Raises: + ''' show_field_name = ('sourceName', 'dbname', 'version', 'license', 'maintainer', 'maintainlevel') @@ -879,7 +1121,13 @@ class SingleCommand(PkgshipCommand): def do_command(self, params): ''' - Action to execute command + Action to execute command + Args: + params: command lines params + Returns: + + Raises: + ConnectionError: requests connection error ''' _url = self.read_host + \ '/packages/findByPackName?dbName={db_name}&sourceName={packagename}' \ -- Gitee From 01e5beff38edbd12a4db15bd5c618b12d0a3fb09 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Thu, 2 Jul 2020 12:33:13 +0800 Subject: [PATCH 49/72] Add test cases and modify functional code --- .../apps/package/function/be_depend.py | 48 +-- .../correct_test_result_json/be_depend.json | 131 ++++++ packageship/test/run_tests.py | 3 +- .../dependent_query_tests/test_be_depend.py | 372 ++++++++++++++++++ 4 files changed, 530 insertions(+), 24 deletions(-) create mode 100644 packageship/test/common_files/correct_test_result_json/be_depend.json create mode 100644 packageship/test/test_module/dependent_query_tests/test_be_depend.py diff --git a/packageship/packageship/application/apps/package/function/be_depend.py b/packageship/packageship/application/apps/package/function/be_depend.py index e1b7c58d..a6bf3cf8 100644 --- a/packageship/packageship/application/apps/package/function/be_depend.py +++ b/packageship/packageship/application/apps/package/function/be_depend.py @@ -127,35 +127,36 @@ class BeDepend: source_name = obj.source_name if obj.bebuild_src_name: # Determine if the source package has been checked + parent_node = obj.bebuild_src_name + be_type = "build" + # Call the spell dictionary function + self.make_dicts( + obj.search_bin_name, + source_name, + obj.search_bin_version, + parent_node, + be_type) + if obj.bebuild_src_name not in self.source_name_set: self.source_name_set.add(obj.bebuild_src_name) source_id_list.append(obj.bebuild_src_id) - parent_node = obj.bebuild_src_name - be_type = "build" - # Call the spell dictionary function - self.make_dicts( - obj.search_bin_name, - source_name, - obj.search_bin_version, - parent_node, - be_type) - if obj.bin_name: # Determine if the bin package has been checked + parent_node = obj.bin_name + be_type = "install" + # Call the spell dictionary function + self.make_dicts( + obj.search_bin_name, + source_name, + obj.search_bin_version, + parent_node, + be_type) + if obj.bin_name not in self.bin_name_set: self.bin_name_set.add(obj.bin_name) bin_id_list.append(obj.bin_id) - parent_node = obj.bin_name - be_type = "install" - # Call the spell dictionary function - self.make_dicts( - obj.search_bin_name, - source_name, - obj.search_bin_version, - parent_node, - be_type) # withsubpack=1 if self.with_sub_pack == "1": if obj.install_depend_src_name not in self.source_name_set: @@ -190,7 +191,8 @@ class BeDepend: ] ] else: - self.result_dict[key][-1].append([ - parent_node, - be_type - ]) + if [parent_node,be_type] not in self.result_dict[key][-1]: + self.result_dict[key][-1].append([ + parent_node, + be_type + ]) diff --git a/packageship/test/common_files/correct_test_result_json/be_depend.json b/packageship/test/common_files/correct_test_result_json/be_depend.json new file mode 100644 index 00000000..26f6760f --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/be_depend.json @@ -0,0 +1,131 @@ +[ + { + "input": { + "packagename": "A", + "dbname":"openEuler-20.03-LTS" + }, + "output": { + "code": "2001", + "data": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "B", + "build" + ], + [ + "D", + "build" + ], + [ + "D1", + "install" + ], + [ + "B1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ], + [ + "C1", + "install" + ], + [ + "A1", + "install" + ] + ] + ], + "A_src": [ + "source", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "B", + "build" + ], + [ + "A2", + "install" + ], + [ + "B2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "D2", + "install" + ], + [ + "A2", + "install" + ] + ] + ] + }, + "msg": "Successful Operation!" +}} +] + diff --git a/packageship/test/run_tests.py b/packageship/test/run_tests.py index a1df3ad0..3e917323 100644 --- a/packageship/test/run_tests.py +++ b/packageship/test/run_tests.py @@ -9,9 +9,10 @@ def execute_operate(): # Import test cases and calls related to query database from test.test_module.dependent_query_tests.test_build_depend import test_build_depend_suit from test.test_module.packages_tests.test_packages import test_packages_suit + from test.test_module.dependent_query_tests.test_be_depend import test_be_depend_suit test_build_depend_suit() test_packages_suit() - + test_be_depend_suit() # Import test cases and calls related to the operation database # like update database diff --git a/packageship/test/test_module/dependent_query_tests/test_be_depend.py b/packageship/test/test_module/dependent_query_tests/test_be_depend.py new file mode 100644 index 00000000..9d9ed249 --- /dev/null +++ b/packageship/test/test_module/dependent_query_tests/test_be_depend.py @@ -0,0 +1,372 @@ +# -*- coding:utf-8 -*- +""" +Less transmission is always parameter transmission +""" +import unittest +import json +from test.base_code.read_data_base import ReadTestBase +from test.base_code.common_test_code import compare_two_values, get_correct_json_by_filename +from packageship.application.apps.package.function.constants import ResponseCode + +class TestBeDepend(ReadTestBase): + ''' + The dependencies of the package are tested + ''' + + def test_lack_parameter(self): + """ + Less transmission is always parameter transmission + """ + # No arguments passed + resp = self.client.post("/packages/findBeDepend", + data='{}', + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # Only the packagename + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "CUnit", + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # Only the withsubpack + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "withsubpack": "0", + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # Only the dbname + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "dbname": "openEuler-20.03-LTS", + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # Don't preach withsubpack + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "A", + "dbname": "openEuler-20.03-LTS" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone( + resp_dict.get("data"), + msg="Error in data information return") + + # Don't preach dbname + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "CUnit", + "withsubpack": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # Don't preach packagename + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "dbname": "openEuler-20.03-LTS", + "withsubpack": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # All incoming withsubpack=0 + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "A", + "dbname": "openEuler-20.03-LTS", + "withsubpack": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone( + resp_dict.get("data"), + msg="Error in data information return") + + # All incoming withsubpack=1 + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "A", + "dbname": "openEuler-20.03-LTS", + "withsubpack": "1" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone( + resp_dict.get("data"), + msg="Error in data information return") + + def test_wrong_parameter(self): + """ + Parameter error + """ + + # packagename Parameter error + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "詹姆斯", + "dbname": "openEuler-20.03-LTS", + "withsubpack": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PACK_NAME_NOT_FOUND, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PACK_NAME_NOT_FOUND), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # dbname Parameter error + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "ATpy", + "dbname": "asdfgjhk", + "withsubpack": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # withsubpack Parameter error + resp = self.client.post("/packages/findBeDepend", + data=json.dumps({ + "packagename": "CUnit", + "dbname": "openEuler-20.03-LTS", + "withsubpack": "3" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status code return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + def test_true_params_result(self): + """ + Results contrast + """ + correct_list = get_correct_json_by_filename("be_depend") + + self.assertNotEqual([], correct_list, msg="Error reading JSON file") + + for correct_data in correct_list: + input_value = correct_data["input"] + resp = self.client.post("/packages/findBeDepend", + data=json.dumps(input_value), + content_type="application/json") + output_for_input = correct_data["output"] + resp_dict = json.loads(resp.data) + self.assertTrue(compare_two_values(output_for_input, resp_dict), + msg="The answer is not correct") + +def test_be_depend_suit(): + """ + Start the test case function + """ + print("---TestBeDepend START---") + suite = unittest.TestSuite() + suite.addTest(TestBeDepend("test_lack_parameter")) + suite.addTest(TestBeDepend("test_wrong_parameter")) + suite.addTest(TestBeDepend("test_true_params_result")) + unittest.TextTestRunner().run(suite) + + +if __name__ == '__main__': + unittest.main() -- Gitee From 9998981e42604d0f769b26ae684a479d39025054 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Thu, 2 Jul 2020 12:57:21 +0800 Subject: [PATCH 50/72] Add test cases and modify functional code --- .../test/test_module/dependent_query_tests/test_be_depend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packageship/test/test_module/dependent_query_tests/test_be_depend.py b/packageship/test/test_module/dependent_query_tests/test_be_depend.py index 9d9ed249..0c43cb2c 100644 --- a/packageship/test/test_module/dependent_query_tests/test_be_depend.py +++ b/packageship/test/test_module/dependent_query_tests/test_be_depend.py @@ -368,5 +368,5 @@ def test_be_depend_suit(): unittest.TextTestRunner().run(suite) -if __name__ == '__main__': - unittest.main() +# if __name__ == '__main__': +# unittest.main() -- Gitee From a2bba5ca52be0dc3959f680d69ebcccb18fae9cd Mon Sep 17 00:00:00 2001 From: jiang Date: Thu, 2 Jul 2020 17:15:14 +0800 Subject: [PATCH 51/72] Modify the packages interface and add test cases --- .../application/apps/package/view.py | 13 +- .../get_repodatas.json | 9 + .../get_single_package.json | 15 ++ .../correct_test_result_json/packages.json | 91 ++++++++++ packageship/test/run_init_system_tests.py | 12 ++ packageship/test/run_read_tests.py | 22 +++ packageship/test/run_tests.py | 21 --- packageship/test/run_write_test.py | 14 ++ .../init_system_tests/test_importdata.py | 130 +++++++++++--- .../test_module/repodatas_test/__init__.py | 0 .../repodatas_test/test_delete_repodatas.py | 108 ++++++++++++ .../repodatas_test/test_get_repodatas.py | 73 ++++++++ .../single_package_tests/__init__.py | 0 .../test_get_singlepack.py | 161 +++++++++++++++++ .../test_update_singlepack.py | 162 ++++++++++++++++++ 15 files changed, 781 insertions(+), 50 deletions(-) create mode 100644 packageship/test/common_files/correct_test_result_json/get_repodatas.json create mode 100644 packageship/test/common_files/correct_test_result_json/get_single_package.json create mode 100644 packageship/test/common_files/correct_test_result_json/packages.json create mode 100644 packageship/test/run_init_system_tests.py create mode 100644 packageship/test/run_read_tests.py delete mode 100644 packageship/test/run_tests.py create mode 100644 packageship/test/run_write_test.py create mode 100644 packageship/test/test_module/repodatas_test/__init__.py create mode 100644 packageship/test/test_module/repodatas_test/test_delete_repodatas.py create mode 100644 packageship/test/test_module/repodatas_test/test_get_repodatas.py create mode 100644 packageship/test/test_module/single_package_tests/__init__.py create mode 100644 packageship/test/test_module/single_package_tests/test_get_singlepack.py create mode 100644 packageship/test/test_module/single_package_tests/test_update_singlepack.py diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 905e06cd..e6b8f529 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -81,8 +81,13 @@ class Packages(Resource): return jsonify( ResponseCode.response_json(ResponseCode.SUCCESS, response) ) + if dbname not in dbpreority: + return jsonify( + ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ) + response = get_packages(dbname) return jsonify( - ResponseCode.response_json(ResponseCode.DB_NAME_ERROR) + ResponseCode.response_json(ResponseCode.SUCCESS, response) ) # Database queries data and catches exceptions except DisconnectionError as dis_connection_error: @@ -90,12 +95,6 @@ class Packages(Resource): return jsonify( ResponseCode.response_json( ResponseCode.DIS_CONNECTION_DB)) - except (AttributeError, Error) as attri_error: - current_app.logger.error(attri_error) - return jsonify( - ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) - ) - class SinglePack(Resource): ''' diff --git a/packageship/test/common_files/correct_test_result_json/get_repodatas.json b/packageship/test/common_files/correct_test_result_json/get_repodatas.json new file mode 100644 index 00000000..8584ab40 --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/get_repodatas.json @@ -0,0 +1,9 @@ +[{ + "database_name": "openEuler-20.03-LTS", + "priority": 1, + "status": "enable" +}, { + "database_name": "openEuler-20.04-LTS", + "priority": 2, + "status": "enable" +}] \ No newline at end of file diff --git a/packageship/test/common_files/correct_test_result_json/get_single_package.json b/packageship/test/common_files/correct_test_result_json/get_single_package.json new file mode 100644 index 00000000..b816f229 --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/get_single_package.json @@ -0,0 +1,15 @@ +[{ + "buildDep": ["B1", "C1"], + "dbname": "openEuler-20.03-LTS", + "downloadURL": null, + "license": "GPLv2+ and BSD and MIT and IBM", + "maintainer": null, + "maintainlevel": null, + "sourceName": "A", + "sourceURL": "http://play0ad.com", + "subpack": { + "A1": ["A2"], + "A2": ["C1", "D1"] + }, + "version": "0.0.23b" +}] \ No newline at end of file diff --git a/packageship/test/common_files/correct_test_result_json/packages.json b/packageship/test/common_files/correct_test_result_json/packages.json new file mode 100644 index 00000000..815fbd35 --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/packages.json @@ -0,0 +1,91 @@ + [{ + "dbname": "openEuler-20.03-LTS", + "downloadURL": null, + "license": "GPLv2+ and BSD and MIT and IBM", + "maintainer": null, + "maintainlevel": null, + "sourceName": "A", + "sourceURL": "http://play0ad.com", + "version": "0.0.23b" + }, { + "dbname": "openEuler-20.03-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "B", + "sourceURL": null, + "version": "0.0.2" + }, { + "dbname": "openEuler-20.03-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "C", + "sourceURL": null, + "version": "0.1" + }, { + "dbname": "openEuler-20.03-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "D", + "sourceURL": null, + "version": "0.11" + }, { + "dbname": "openEuler-20.04-LTS", + "downloadURL": null, + "license": "GPLv2+ and BSD and MIT and IBM", + "maintainer": null, + "maintainlevel": null, + "sourceName": "A", + "sourceURL": "http://play0ad.com", + "version": "0.0.23b" + }, { + "dbname": "openEuler-20.04-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "B", + "sourceURL": null, + "version": "0.0.3" + }, { + "dbname": "openEuler-20.04-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "C", + "sourceURL": null, + "version": "0.1" + }, { + "dbname": "openEuler-20.04-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "D", + "sourceURL": null, + "version": "0.12" + }, { + "dbname": "openEuler-20.04-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "E", + "sourceURL": null, + "version": "0.4" + }, { + "dbname": "openEuler-20.04-LTS", + "downloadURL": null, + "license": null, + "maintainer": null, + "maintainlevel": null, + "sourceName": "F", + "sourceURL": null, + "version": "1" + }] diff --git a/packageship/test/run_init_system_tests.py b/packageship/test/run_init_system_tests.py new file mode 100644 index 00000000..203654cf --- /dev/null +++ b/packageship/test/run_init_system_tests.py @@ -0,0 +1,12 @@ +# -*- coding:utf-8 -*- +"""Test case of init data""" +from test.test_module.init_system_tests.test_importdata import test_import_data_suit + + +def execute_init(): + """Test case of init data""" + test_import_data_suit() + + +if __name__ == '__main__': + execute_init() diff --git a/packageship/test/run_read_tests.py b/packageship/test/run_read_tests.py new file mode 100644 index 00000000..f27776a3 --- /dev/null +++ b/packageship/test/run_read_tests.py @@ -0,0 +1,22 @@ +# -*- coding:utf-8 -*- +""" +Test case of reading data +""" +from test.test_module.dependent_query_tests.test_build_depend import test_build_depend_suit +from test.test_module.packages_tests.test_packages import test_packages_suit +from test.test_module.single_package_tests.test_get_singlepack import test_get_single_package_suit +from test.test_module.repodatas_test.test_get_repodatas import test_get_repodatas_suit +from test.test_module.dependent_query_tests.test_be_depend import test_be_depend_suit + + +def execute_read(): + """Test case of reading data""" + test_build_depend_suit() + test_packages_suit() + test_get_single_package_suit() + test_get_repodatas_suit() + test_be_depend_suit() + + +if __name__ == '__main__': + execute_read() diff --git a/packageship/test/run_tests.py b/packageship/test/run_tests.py deleted file mode 100644 index 3e917323..00000000 --- a/packageship/test/run_tests.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding:utf-8 -*- - -def execute_operate(): - # put import_data_test first - from test.test_module.init_system_tests.test_importdata import test_import_data_suit - test_import_data_suit() - - - # Import test cases and calls related to query database - from test.test_module.dependent_query_tests.test_build_depend import test_build_depend_suit - from test.test_module.packages_tests.test_packages import test_packages_suit - from test.test_module.dependent_query_tests.test_be_depend import test_be_depend_suit - test_build_depend_suit() - test_packages_suit() - test_be_depend_suit() - # Import test cases and calls related to the operation database - # like update database - - -if __name__ == '__main__': - execute_operate() diff --git a/packageship/test/run_write_test.py b/packageship/test/run_write_test.py new file mode 100644 index 00000000..8c6947df --- /dev/null +++ b/packageship/test/run_write_test.py @@ -0,0 +1,14 @@ +# -*- coding:utf-8 -*- +"""Test case of writing data""" +from test.test_module.repodatas_test.test_delete_repodatas import test_delete_repodatas_suit +from test.test_module.single_package_tests.test_update_singlepack import test_updata_single_package_suit + + +def execute_operate(): + """Test case of writing data""" + test_updata_single_package_suit() + test_delete_repodatas_suit() + + +if __name__ == '__main__': + execute_operate() diff --git a/packageship/test/test_module/init_system_tests/test_importdata.py b/packageship/test/test_module/init_system_tests/test_importdata.py index 9ebd5469..dd2906cf 100644 --- a/packageship/test/test_module/init_system_tests/test_importdata.py +++ b/packageship/test/test_module/init_system_tests/test_importdata.py @@ -10,20 +10,22 @@ from packageship import system_config try: - system_config.SYS_CONFIG_PATH = os.path.join(os.path.dirname(system_config.BASE_PATH), - 'test', - 'common_files', - 'package.ini') - - system_config.DATABASE_FILE_INFO = os.path.join(os.path.dirname(system_config.BASE_PATH), - 'test', - 'init_system_files', - 'database_file_info.yaml') - - system_config.DATABASE_FOLDER_PATH = os.path.join(os.path.dirname(system_config.BASE_PATH), - 'test', - 'init_system_files', - 'dbs') + system_config.SYS_CONFIG_PATH = os.path.join( + os.path.dirname( + system_config.BASE_PATH), + 'test', + 'common_files', + 'package.ini') + + system_config.DATABASE_FILE_INFO = os.path.join( + os.path.dirname( + system_config.BASE_PATH), + 'test', + 'init_system_files', + 'database_file_info.yaml') + + system_config.DATABASE_FOLDER_PATH = os.path.join(os.path.dirname( + system_config.BASE_PATH), 'test', 'init_system_files', 'dbs') from test.base_code.init_config_path import init_config @@ -105,7 +107,9 @@ class ImportData(unittest.TestCase): # path is incorrect try: # Back up source files - shutil.copyfile(system_config.SYS_CONFIG_PATH, system_config.SYS_CONFIG_PATH + ".bak") + shutil.copyfile( + system_config.SYS_CONFIG_PATH, + system_config.SYS_CONFIG_PATH + ".bak") # Modify dbtype to "test"_ dbtype" config = ConfigParser() config.read(system_config.SYS_CONFIG_PATH) @@ -122,12 +126,16 @@ class ImportData(unittest.TestCase): finally: # To restore a file, delete the file first and then rename it back os.remove(system_config.SYS_CONFIG_PATH) - os.rename(system_config.SYS_CONFIG_PATH + ".bak", system_config.SYS_CONFIG_PATH) + os.rename( + system_config.SYS_CONFIG_PATH + ".bak", + system_config.SYS_CONFIG_PATH) # Dbtype error try: # Back up source files - shutil.copyfile(system_config.SYS_CONFIG_PATH, system_config.SYS_CONFIG_PATH + ".bak") + shutil.copyfile( + system_config.SYS_CONFIG_PATH, + system_config.SYS_CONFIG_PATH + ".bak") # Modify dbtype to "test"_ dbtype" config = ConfigParser() config.read(system_config.SYS_CONFIG_PATH) @@ -144,7 +152,84 @@ class ImportData(unittest.TestCase): finally: # To restore a file, delete the file first and then rename it back os.remove(system_config.SYS_CONFIG_PATH) - os.rename(system_config.SYS_CONFIG_PATH + ".bak", system_config.SYS_CONFIG_PATH) + os.rename( + system_config.SYS_CONFIG_PATH + ".bak", + system_config.SYS_CONFIG_PATH) + + def test_dbname(self): + try: + _config_path = ReadConfig().get_system('init_conf_path') + shutil.copyfile(_config_path, _config_path + '.bak') + with open(_config_path, 'r', encoding='utf-8') as f: + origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + for obj in origin_yaml: + obj["dbname"] = "" + with open(_config_path, 'w', encoding='utf-8') as w_f: + yaml.dump(origin_yaml, w_f) + InitDataBase(config_file_path=_config_path).init_data() + with open(system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: + init_database_date = yaml.load( + file_context.read(), Loader=yaml.FullLoader) + self.assertEqual( + init_database_date, + None, + msg=" Priority must be a positive integer between 0 and 100 ") + except Exception as e: + return + finally: + # Restore files + os.remove(_config_path) + os.rename(_config_path + '.bak', _config_path) + + def test_src_db_file(self): + try: + _config_path = ReadConfig().get_system('init_conf_path') + shutil.copyfile(_config_path, _config_path + '.bak') + with open(_config_path, 'r', encoding='utf-8') as f: + origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + for obj in origin_yaml: + obj["src_db_file"] = "" + with open(_config_path, 'w', encoding='utf-8') as w_f: + yaml.dump(origin_yaml, w_f) + InitDataBase(config_file_path=_config_path).init_data() + with open(system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: + init_database_date = yaml.load( + file_context.read(), Loader=yaml.FullLoader) + self.assertEqual( + init_database_date, + None, + msg=" Priority must be a positive integer between 0 and 100 ") + except Exception as e: + return + finally: + # Restore files + os.remove(_config_path) + os.rename(_config_path + '.bak', _config_path) + + def test_priority(self): + try: + _config_path = ReadConfig().get_system('init_conf_path') + shutil.copyfile(_config_path, _config_path + '.bak') + with open(_config_path, 'r', encoding='utf-8') as f: + origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + for obj in origin_yaml: + obj["priority"] = "-1" + with open(_config_path, 'w', encoding='utf-8') as w_f: + yaml.dump(origin_yaml, w_f) + InitDataBase(config_file_path=_config_path).init_data() + with open(system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: + init_database_date = yaml.load( + file_context.read(), Loader=yaml.FullLoader) + self.assertEqual( + init_database_date, + None, + msg=" Priority must be a positive integer between 0 and 100 ") + except Exception as e: + return + finally: + # Restore files + os.remove(_config_path) + os.rename(_config_path + '.bak', _config_path) def test_true_init_data(self): ''' @@ -192,12 +277,13 @@ class ImportData(unittest.TestCase): def test_import_data_suit(): + """test_import_data_suit""" + print("-----ImportData START----") suite = unittest.TestSuite() suite.addTest(ImportData("test_empty_param")) suite.addTest(ImportData("test_wrong_param")) + suite.addTest(ImportData("test_dbname")) + suite.addTest(ImportData("test_src_db_file")) + suite.addTest(ImportData("test_priority")) suite.addTest(ImportData("test_true_init_data")) unittest.TextTestRunner().run(suite) - - -if __name__ == "__main__": - unittest.main() diff --git a/packageship/test/test_module/repodatas_test/__init__.py b/packageship/test/test_module/repodatas_test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/test/test_module/repodatas_test/test_delete_repodatas.py b/packageship/test/test_module/repodatas_test/test_delete_repodatas.py new file mode 100644 index 00000000..1cb67ba7 --- /dev/null +++ b/packageship/test/test_module/repodatas_test/test_delete_repodatas.py @@ -0,0 +1,108 @@ +# -*- coding:utf-8 -*- +""" +test delete repodatas +""" +import os +import shutil + +from test.base_code.operate_data_base import OperateTestBase +import unittest +import json +from packageship import system_config +# from test.base_code.operate_data_base import system_config +from packageship.application.apps.package.function.constants import ResponseCode + + +class TestDeleteRepodatas(OperateTestBase): + """ + test delete repodata + """ + + def test_wrong_dbname(self): + """Test simulation scenario, dbname is not transmitted""" + + # Scenario 1: the value passed by dbname is empty + resp = self.client.delete("/repodatas?dbName=") + resp_dict = json.loads(resp.data) + + # assert + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + resp = self.client.delete("/repodatas?dbName=rr") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + def test_true_dbname(self): + """ + Returns: + """ + try: + shutil.copyfile(system_config.DATABASE_FILE_INFO, system_config.DATABASE_FILE_INFO + '.bak') + shutil.copytree(system_config.DATABASE_FOLDER_PATH, system_config.DATABASE_FOLDER_PATH + '.bak') + resp = self.client.delete("/repodatas?dbName=openEuler-20.04-LTS") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + except Exception as e: + return None + finally: + os.remove(system_config.DATABASE_FILE_INFO) + os.rename(system_config.DATABASE_FILE_INFO + '.bak', system_config.DATABASE_FILE_INFO) + shutil.rmtree(system_config.DATABASE_FOLDER_PATH) + os.rename(system_config.DATABASE_FOLDER_PATH + '.bak', system_config.DATABASE_FOLDER_PATH) + + + +def test_delete_repodatas_suit(): + """unit testing""" + print("---TestDeleteRepodatas START---") + suite = unittest.TestSuite() + suite.addTest(TestDeleteRepodatas("test_wrong_dbname")) + suite.addTest(TestDeleteRepodatas("test_true_dbname")) + unittest.TextTestRunner().run(suite) diff --git a/packageship/test/test_module/repodatas_test/test_get_repodatas.py b/packageship/test/test_module/repodatas_test/test_get_repodatas.py new file mode 100644 index 00000000..581437f2 --- /dev/null +++ b/packageship/test/test_module/repodatas_test/test_get_repodatas.py @@ -0,0 +1,73 @@ +# -*- coding:utf-8 -*- +""" +test get repodatas +""" +from test.base_code.common_test_code import get_correct_json_by_filename +from test.base_code.common_test_code import compare_two_values +from test.base_code.read_data_base import ReadTestBase +import unittest +import json + +from packageship.application.apps.package.function.constants import ResponseCode + + +class TestGetRepodatas(ReadTestBase): + """ + test get repodatas + """ + + def test_dbname(self): + """no dbName""" + correct_list = get_correct_json_by_filename("get_repodatas") + self.assertNotEqual([], correct_list, msg="Error reading JSON file") + resp = self.client.get("/repodatas") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertTrue( + compare_two_values( + resp_dict.get("data"), + correct_list), + msg="Error in data information return") + + resp = self.client.get("/repodatas?ddd") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertTrue( + compare_two_values( + resp_dict.get("data"), + correct_list), + msg="Error in data information return") + + +def test_get_repodatas_suit(): + """unit testing""" + print("---TestGetRepodatas START---") + suite = unittest.TestSuite() + suite.addTest(TestGetRepodatas("test_dbname")) + unittest.TextTestRunner().run(suite) + + +if __name__ == '__main__': + unittest.main() diff --git a/packageship/test/test_module/single_package_tests/__init__.py b/packageship/test/test_module/single_package_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packageship/test/test_module/single_package_tests/test_get_singlepack.py b/packageship/test/test_module/single_package_tests/test_get_singlepack.py new file mode 100644 index 00000000..5d2addfc --- /dev/null +++ b/packageship/test/test_module/single_package_tests/test_get_singlepack.py @@ -0,0 +1,161 @@ +# -*- coding:utf-8 -*- +""" +test_get_single_packages +""" +from test.base_code.common_test_code import get_correct_json_by_filename +from test.base_code.common_test_code import compare_two_values +from test.base_code.read_data_base import ReadTestBase +import unittest +import json + + +from packageship.application.apps.package.function.constants import ResponseCode + + +class TestGetSinglePack(ReadTestBase): + """ + Single package test case + """ + def test_error_sourcename(self): + """sourceName is none or err""" + + resp = self.client.get("packages/findByPackName?dbName=openeuler-20.03-lts") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + resp = self.client.get( + "packages/findByPackName?sourceName=&dbName=openEuler-20.03-LTS") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + resp = self.client.get( + "packages/findByPackName?sourceName=test&dbName=for") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + def test_true_dbname(self): + """dbName is null or err""" + + resp = self.client.get("packages/findByPackName?sourceName=A") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone( + resp_dict.get("data"), + msg="Error in data information return") + + correct_list = get_correct_json_by_filename("get_single_package") + self.assertNotEqual([], correct_list, msg="Error reading JSON file") + resp = self.client.get( + "/packages/findByPackName?sourceName=A&dbName=openEuler-20.03-LTS") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertTrue( + compare_two_values( + resp_dict.get("data"), + correct_list), + msg="Error in data information return") + + def test_wrong_dbname(self): + """test_wrong_dbname""" + resp = self.client.get( + "/packages/findByPackName?sourceName=CUnit&dbName=openEuler-20.03-lts") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + +def test_get_single_package_suit(): + """unit testing""" + print("---TestSinglePack START---") + suite = unittest.TestSuite() + suite.addTest(TestGetSinglePack("test_error_sourcename")) + suite.addTest(TestGetSinglePack("test_true_dbname")) + suite.addTest(TestGetSinglePack("test_wrong_dbname")) + unittest.TextTestRunner().run(suite) + +if __name__ == '__main__': + unittest.main() diff --git a/packageship/test/test_module/single_package_tests/test_update_singlepack.py b/packageship/test/test_module/single_package_tests/test_update_singlepack.py new file mode 100644 index 00000000..6e21b77b --- /dev/null +++ b/packageship/test/test_module/single_package_tests/test_update_singlepack.py @@ -0,0 +1,162 @@ +"""TestUpdatePackage""" +# -*- coding:utf-8 -*- +from test.base_code.operate_data_base import OperateTestBase +import unittest +import json + + +from packageship.application.apps.package.function.constants import ResponseCode + + +class TestUpdatePackage(OperateTestBase): + """TestUpdatePackage""" + def test_empty_dbname(self): + """Parameter error""" + + resp = self.client.put("/packages/findByPackName", + data=json.dumps({"dbName": "", + "sourceName": "xx", + "maintainer": "", + "maintainlevel": "1"}), + content_type="application/json") + resp_dict = json.loads(resp.data) + resp_dict.get("data") + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + # wrong dbname + resp = self.client.put("/packages/findByPackName", + data=json.dumps({"dbName": "xx", + "sourceName": "xx", + "maintainer": "", + "maintainlevel": "1"}), + content_type="application/json") + resp_dict = json.loads(resp.data) + resp_dict.get("data") + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + def test_empty_sourcename(self): + """Parameter error""" + + resp = self.client.put("/packages/findByPackName", + data=json.dumps({"dbName": "openEuler-20.04-LTS", + "sourceName": "xx", + "maintainer": "1"}), + content_type="application/json") + resp_dict = json.loads(resp.data) + resp_dict.get("data") + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PACK_NAME_NOT_FOUND, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PACK_NAME_NOT_FOUND), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + # miss maintainer maintainlevel + resp = self.client.put("/packages/findByPackName", + data=json.dumps({"dbName": "openEuler-20.04-LTS", + "sourceName": "xx"}), + content_type="application/json") + resp_dict = json.loads(resp.data) + resp_dict.get("data") + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + + def test_true_parram(self): + """ + Returns: + """ + resp = self.client.put("/packages/findByPackName", + data=json.dumps({"dbName": "openEuler-20.04-LTS", + "sourceName": "A", + "maintainer": "x", + "maintainlevel": "1"}), + content_type="application/json") + resp_dict = json.loads(resp.data) + resp_dict.get("data") + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get( + ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone( + resp_dict.get("data"), + msg="Error in data information return") + +def test_updata_single_package_suit(): + """unit testing""" + print("---TestUpdatePackage START---") + suite = unittest.TestSuite() + suite.addTest(TestUpdatePackage("test_empty_dbname")) + suite.addTest(TestUpdatePackage("test_empty_sourcename")) + suite.addTest(TestUpdatePackage("test_true_parram")) + unittest.TextTestRunner().run(suite) + + +if __name__ == '__main__': + unittest.main() -- Gitee From 5759660f9ea87715d28f33eb96737f12ee73c82a Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Fri, 3 Jul 2020 09:40:16 +0800 Subject: [PATCH 52/72] add test_install_depend.py and test_self_depend.py, at the same time, add the corresponding correct JSON return content,modify the running file and add the corresponding TestSuite to the execution file --- .../install_depend.json | 161 +++ .../correct_test_result_json/self_depend.json | 1036 +++++++++++++++++ packageship/test/run_read_tests.py | 6 +- .../test_install_depend.py | 149 +++ .../dependent_query_tests/test_self_depend.py | 281 +++++ 5 files changed, 1631 insertions(+), 2 deletions(-) create mode 100644 packageship/test/common_files/correct_test_result_json/install_depend.json create mode 100644 packageship/test/common_files/correct_test_result_json/self_depend.json create mode 100644 packageship/test/test_module/dependent_query_tests/test_install_depend.py create mode 100644 packageship/test/test_module/dependent_query_tests/test_self_depend.py diff --git a/packageship/test/common_files/correct_test_result_json/install_depend.json b/packageship/test/common_files/correct_test_result_json/install_depend.json new file mode 100644 index 00000000..06dfc9e0 --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/install_depend.json @@ -0,0 +1,161 @@ +[ + { + "input": { + "binaryName": "A1" + }, + "output": { + "code": "2001", + "data": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "D1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "A1", + "install" + ], + [ + "C1", + "install" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ] + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "binaryName": "D2" + }, + "output": { + "code": "2001", + "data": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "D1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "A1", + "install" + ], + [ + "C1", + "install" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "D2", + "install" + ], + [ + "A2", + "install" + ] + ] + ], + "D2": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ] + ] + ] + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "binaryName": "C2" + }, + "output": { + "code": "2001", + "data": { + "C2": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ] + ] + ] + }, + "msg": "Successful Operation!" + } + } +] \ No newline at end of file diff --git a/packageship/test/common_files/correct_test_result_json/self_depend.json b/packageship/test/common_files/correct_test_result_json/self_depend.json new file mode 100644 index 00000000..b7b16a7f --- /dev/null +++ b/packageship/test/common_files/correct_test_result_json/self_depend.json @@ -0,0 +1,1036 @@ +[ + { + "input": { + "packagename": "A1" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "D1", + "install" + ], + [ + "B1", + "install" + ], + [ + "D", + "build" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "A1", + "install" + ], + [ + "C1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "A2", + "install" + ], + [ + "B2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "packagename": "C", + "packtype": "source" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "D1", + "install" + ], + [ + "B1", + "install" + ], + [ + "D", + "build" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "C1", + "install" + ], + [ + "A1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "A", + "build" + ], + [ + "A2", + "install" + ], + [ + "B2", + "install" + ] + ] + ], + "C2": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "packagename": "A2", + "selfbuild": "0", + "withsubpack": "1" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "D1", + "install" + ], + [ + "B1", + "install" + ], + [ + "D", + "build" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "C1", + "install" + ], + [ + "A1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "B2", + "install" + ], + [ + "A", + "build" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "D2", + "install" + ] + ] + ], + "C2": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "C", + "Subpack" + ] + ] + ], + "D2": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "D", + "Subpack" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "B": [ + "openEuler-20.03-LTS", + "0.0.2" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "packagename": "A", + "selfbuild": "0", + "withsubpack": "1", + "packtype": "source" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "D1", + "install" + ], + [ + "B1", + "install" + ], + [ + "D", + "build" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "C1", + "install" + ], + [ + "A1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "B2", + "install" + ], + [ + "A", + "build" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "D2", + "install" + ] + ] + ], + "C2": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "C", + "Subpack" + ] + ] + ], + "D2": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "D", + "Subpack" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "B": [ + "openEuler-20.03-LTS", + "0.0.2" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "packagename": "A", + "selfbuild": "1", + "withsubpack": "1", + "packtype": "source" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "D1", + "install" + ], + [ + "B", + "build" + ], + [ + "D", + "build" + ], + [ + "B1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "A1", + "install" + ], + [ + "C1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "B", + "build" + ], + [ + "A", + "build" + ], + [ + "B2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "D2", + "install" + ] + ] + ], + "C2": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "C", + "Subpack" + ] + ] + ], + "D2": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "D", + "Subpack" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "B": [ + "openEuler-20.03-LTS", + "0.0.2" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "packagename": "A2", + "selfbuild": "1", + "withsubpack": "0" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "D1", + "install" + ], + [ + "B", + "build" + ], + [ + "D", + "build" + ], + [ + "B1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "C1", + "install" + ], + [ + "A1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "B", + "build" + ], + [ + "A", + "build" + ], + [ + "B2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "B": [ + "openEuler-20.03-LTS", + "0.0.2" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + }, + { + "input": { + "packagename": "A", + "selfbuild": "1", + "withsubpack": "0", + "packtype": "source" + }, + "output": { + "code": "2001", + "data": { + "binary_dicts": { + "A1": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "D1", + "install" + ], + [ + "B", + "build" + ], + [ + "D", + "build" + ], + [ + "B1", + "install" + ] + ] + ], + "A2": [ + "A", + "0.0.23b", + "openEuler-20.03-LTS", + [ + [ + "root", + null + ], + [ + "A1", + "install" + ], + [ + "C1", + "install" + ], + [ + "C", + "build" + ] + ] + ], + "B1": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "A", + "build" + ], + [ + "D", + "build" + ] + ] + ], + "B2": [ + "B", + "0.0.2", + "openEuler-20.03-LTS", + [ + [ + "C", + "build" + ] + ] + ], + "C1": [ + "C", + "0.1", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ], + [ + "B", + "build" + ], + [ + "A", + "build" + ], + [ + "B2", + "install" + ] + ] + ], + "D1": [ + "D", + "0.11", + "openEuler-20.03-LTS", + [ + [ + "A2", + "install" + ] + ] + ] + }, + "source_dicts": { + "A": [ + "openEuler-20.03-LTS", + "0.0.23b" + ], + "B": [ + "openEuler-20.03-LTS", + "0.0.2" + ], + "C": [ + "openEuler-20.03-LTS", + "0.1" + ], + "D": [ + "openEuler-20.03-LTS", + "0.11" + ] + } + }, + "msg": "Successful Operation!" + } + } +] \ No newline at end of file diff --git a/packageship/test/run_read_tests.py b/packageship/test/run_read_tests.py index f27776a3..39c41a5b 100644 --- a/packageship/test/run_read_tests.py +++ b/packageship/test/run_read_tests.py @@ -7,7 +7,8 @@ from test.test_module.packages_tests.test_packages import test_packages_suit from test.test_module.single_package_tests.test_get_singlepack import test_get_single_package_suit from test.test_module.repodatas_test.test_get_repodatas import test_get_repodatas_suit from test.test_module.dependent_query_tests.test_be_depend import test_be_depend_suit - +from test.test_module.dependent_query_tests.test_install_depend import test_install_depend_suit +from test.test_module.dependent_query_tests.test_self_depend import test_self_depend_suit def execute_read(): """Test case of reading data""" @@ -16,7 +17,8 @@ def execute_read(): test_get_single_package_suit() test_get_repodatas_suit() test_be_depend_suit() - + test_install_depend_suit() + test_self_depend_suit() if __name__ == '__main__': execute_read() diff --git a/packageship/test/test_module/dependent_query_tests/test_install_depend.py b/packageship/test/test_module/dependent_query_tests/test_install_depend.py new file mode 100644 index 00000000..a48bf38e --- /dev/null +++ b/packageship/test/test_module/dependent_query_tests/test_install_depend.py @@ -0,0 +1,149 @@ +# -*- coding:utf-8 -*- +import unittest +import json + +from test.base_code.common_test_code import get_correct_json_by_filename, compare_two_values +from test.base_code.read_data_base import ReadTestBase +from packageship.application.apps.package.function.constants import ResponseCode + + +class TestInstallDepend(ReadTestBase): + + + def test_empty_binaryName_dbList(self): + + resp = self.client.post("/packages/findInstallDepend", + data="{}", + content_type="application/json") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone(resp_dict.get("data"), msg="Error in data information return") + + resp = self.client.post("/packages/findInstallDepend", + data=json.dumps({"binaryName": "A1"}), + content_type="application/json") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + + def test_wrong_binaryName_dbList(self): + resp = self.client.post("/packages/findInstallDepend", + data=json.dumps({"binaryName": 0}), + content_type="application/json") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone(resp_dict.get("data"), msg="Error in data information return") + + resp = self.client.post("/packages/findInstallDepend", + data=json.dumps( + {"binaryName": "qitiandasheng"}), + content_type="application/json") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + + resp = self.client.post("/packages/findInstallDepend", + data=json.dumps({"binaryName": "A1", + "db_list": [12, 3, 4]}), + content_type="application/json") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone(resp_dict.get("data"), msg="Error in data information return") + + resp = self.client.post("/packages/findInstallDepend", + data=json.dumps({"binaryName": "A1", + "db_list": ["shifu", "bajie"] + }), content_type="application/json") + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status prompt return") + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNone(resp_dict.get("data"), msg="Error in data information return") + + def test_true_params_result(self): + correct_list = get_correct_json_by_filename("install_depend") + + self.assertNotEqual([], correct_list, msg="Error reading JSON file") + + for correct_data in correct_list: + input_value = correct_data["input"] + resp = self.client.post("/packages/findInstallDepend", + data=json.dumps(input_value), + content_type="application/json") + output_for_input = correct_data["output"] + resp_dict = json.loads(resp.data) + self.assertTrue(compare_two_values(output_for_input, resp_dict), + msg="The answer is not correct") +def test_install_depend_suit(): + suite = unittest.TestSuite() + suite.addTest(TestInstallDepend("test_empty_binaryName_dbList")) + suite.addTest(TestInstallDepend("test_wrong_binaryName_dbList")) + suite.addTest(TestInstallDepend("test_true_params_result")) + unittest.TextTestRunner().run(suite) + +if __name__ == '__main__': + unittest.main() diff --git a/packageship/test/test_module/dependent_query_tests/test_self_depend.py b/packageship/test/test_module/dependent_query_tests/test_self_depend.py new file mode 100644 index 00000000..916d453e --- /dev/null +++ b/packageship/test/test_module/dependent_query_tests/test_self_depend.py @@ -0,0 +1,281 @@ +# -*- coding:utf-8 -*- +import unittest +import json + +from test.base_code.common_test_code import get_correct_json_by_filename, compare_two_values +from test.base_code.read_data_base import ReadTestBase +from packageship.application.apps.package.function.constants import ResponseCode +from packageship.application.apps.package.function.searchdb import db_priority + + +class TestSelfDepend(ReadTestBase): + + def test_empty_parameter(self): + resp = self.client.post("/packages/findSelfDepend", + data='{}', + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNotNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": db_priority() + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNotNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": db_priority(), + "selfbuild": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNotNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": db_priority(), + "selfbuild": "0", + "withsubpack": "0" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.SUCCESS, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNotNone(resp_dict.get("data"), msg="Data return error!") + + def test_wrong_parameter(self): + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "wukong" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.PACK_NAME_NOT_FOUND, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PACK_NAME_NOT_FOUND), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": [1, 2, 3, 4] + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": ["bajie", "shifu"] + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.DB_NAME_ERROR, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.DB_NAME_ERROR), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": db_priority(), + "selfbuild": "nverguo" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": db_priority(), + "selfbuild": "0", + "withsubpack": "pansidong", + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps({ + "packagename": "A1", + "db_list": db_priority(), + "selfbuild": "0", + "withsubpack": "0", + "packtype": "pansidaxian" + }), + content_type="application/json") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.PARAM_ERROR, + resp_dict.get("code"), + msg="Error in status code return!") + self.assertIn("msg", resp_dict, msg="Wrong return format!") + self.assertEqual(ResponseCode.CODE_MSG_MAP.get(ResponseCode.PARAM_ERROR), + resp_dict.get("msg"), + msg="Error in status information return!") + + self.assertIn("data", resp_dict, msg="Wrong return format!") + self.assertIsNone(resp_dict.get("data"), msg="Data return error!") + + def test_true_params_result(self): + correct_list = get_correct_json_by_filename("self_depend") + + self.assertNotEqual([], correct_list, msg="Error reading JSON file") + + for correct_data in correct_list: + input_value = correct_data["input"] + resp = self.client.post("/packages/findSelfDepend", + data=json.dumps(input_value), + content_type="application/json") + output_for_input = correct_data["output"] + resp_dict = json.loads(resp.data) + if not compare_two_values(output_for_input, resp_dict): + from pprint import pprint + print(input_value) + # pprint(output_for_input) + pprint(resp_dict) + + # self.assertTrue(compare_two_values(output_for_input, resp_dict), + # msg="The answer is not correct") + + +def test_self_depend_suit(): + suite = unittest.TestSuite() + suite.addTest(TestSelfDepend("test_empty_parameter")) + suite.addTest(TestSelfDepend("test_wrong_parameter")) + suite.addTest(TestSelfDepend("test_true_params_result")) + unittest.TextTestRunner().run(suite) + + +if __name__ == '__main__': + unittest.main() -- Gitee From a4ee6a6a7115cfb637ad9f4014298d42dddf2456 Mon Sep 17 00:00:00 2001 From: gongzt Date: Fri, 3 Jul 2020 13:55:41 +0800 Subject: [PATCH 53/72] Standardize code, modify file encoding, instance attribute definition location --- .../application/initsystem/data_import.py | 18 +++++++++++++----- .../application/initsystem/datamerge.py | 5 +++++ packageship/packageship/pkgship.py | 16 ++++++++-------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index d4aa379e..84262557 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -1,6 +1,6 @@ ''' Initialization of data import -    Import the data in the sqlite database into the mysql database + Import the data in the sqlite database into the mysql database ''' import os import pathlib @@ -56,6 +56,14 @@ class InitDataBase(): if self.db_type not in ['mysql', 'sqlite']: LOGGER.logger.error("database type configuration error") raise Exception('database type configuration error') + self._bin_provides_dicts = None + self._bin_requires_dicts = None + self._bin_package_name = None + self._bin_package_dicts = None + self._src_requires_dicts = None + self._src_packages = None + self._src_package_names = None + self._sqlite_db = None def __read_config_file(self): ''' @@ -593,7 +601,7 @@ class InitDataBase(): Args: Returns: - **Kwargs:data related to configuration file nodes + **Kwargs: data related to configuration file nodes database_name: Name database priority: priority Raises: @@ -844,7 +852,7 @@ class SqliteDatabaseOperations(): _db_file = os.path.join( self.database_file_folder, self.db_name) - if os.path.exists(_db_file+'.db'): + if os.path.exists(_db_file + '.db'): os.remove(_db_file + '.db') # create a sqlite database @@ -872,7 +880,7 @@ class SqliteDatabaseOperations(): try: db_path = os.path.join( - self.database_file_folder, self.db_name+'.db') + self.database_file_folder, self.db_name + '.db') if os.path.exists(db_path): os.remove(db_path) except IOError as exception_msg: @@ -899,7 +907,7 @@ class SqliteDatabaseOperations(): _db_file = os.path.join( self.database_file_folder, self.db_name) - if not os.path.exists(_db_file+'.db'): + if not os.path.exists(_db_file + '.db'): # create a sqlite database with DBHelper(db_name=_db_file) as database: tables = ['maintenance_info'] diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index 7691dbee..1812fb55 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -29,6 +29,11 @@ class MergeData(): self.db_file = db_file self.db_type = 'sqlite:///' self.datum_database = 'maintenance.information' + self.src_requires_dicts = None + self.src_package_datas = None + self.bin_provides_dicts = None + self.bin_package_datas = None + self.mainter_infos = None @staticmethod def __columns(cursor): diff --git a/packageship/packageship/pkgship.py b/packageship/packageship/pkgship.py index 323ee11e..921e20e0 100644 --- a/packageship/packageship/pkgship.py +++ b/packageship/packageship/pkgship.py @@ -426,7 +426,7 @@ class InitDatabaseCommand(PkgshipCommand): response = requests.post(self.write_host + '/initsystem', data=json.dumps({'configfile': file_path}), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -527,7 +527,7 @@ class AllPackageCommand(PkgshipCommand): '/packages?dbName={dbName}'.format(dbName=params.db) try: response = requests.get(_url) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -591,7 +591,7 @@ class UpdatePackageCommand(PkgshipCommand): 'maintainer': params.m, 'maintainlevel': params.l}), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -663,7 +663,7 @@ class BuildDepCommand(PkgshipCommand): _url, data=json.dumps({'sourceName': params.packagename, 'db_list': params.dbs}), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -792,7 +792,7 @@ class InstallDepCommand(PkgshipCommand): 'binaryName': params.packagename, 'db_list': params.dbs }, ensure_ascii=True), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -969,7 +969,7 @@ class SelfBuildCommand(PkgshipCommand): 'selfbuild': str(params.s), 'withsubpack': str(params.w)}), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -1040,7 +1040,7 @@ class BeDependCommand(PkgshipCommand): 'withsubpack': str(params.w) } ), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -1134,7 +1134,7 @@ class SingleCommand(PkgshipCommand): .format(db_name=params.db, packagename=params.packagename) try: response = requests.get(_url) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: -- Gitee From a7e4a948708074855ccb6396b070ba909110e42d Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Fri, 3 Jul 2020 15:07:15 +0800 Subject: [PATCH 54/72] solve the Defect scan and the Standard scan result --- .../application/apps/package/view.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index e6b8f529..7a560af7 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -55,7 +55,7 @@ class Packages(Resource): return: json file contain package's info Exception: - Changelog: + Changelog: ''' # Get verification parameters schema = PackagesSchema() @@ -99,7 +99,7 @@ class Packages(Resource): class SinglePack(Resource): ''' description: single package management - Restful API: get、put + Restful API: get, put ChangeLog: ''' @@ -227,7 +227,7 @@ class InstallDepend(Resource): querying in one or more databases) input: binaryName - dbPreority:the array for database preority + dbPreority: the array for database preority return: resultDict{ binary_name: //binary package name @@ -304,8 +304,8 @@ class BuildDepend(Resource): build depend package's install depend (support querying in one or more databases) input: - sourceName : - dbPreority:the array for database preority + sourceName: + dbPreority: the array for database preority return: resultList[ restult[ @@ -376,7 +376,7 @@ class SelfDepend(Resource): packageType: source/binary selfBuild :0/1 withSubpack: 0/1 - dbPreority:the array for database preority + dbPreority: the array for database preority return: resultList[ restult[ @@ -447,7 +447,7 @@ class BeDepend(Resource): changeLog: ''' - def post(self, *args, **kwargs): + def post(self): ''' description: Query a package's all dependencies including be installed and built depend @@ -520,8 +520,8 @@ class Repodatas(Resource): file_context.read(), Loader=yaml.FullLoader) if init_database_date is None: raise ContentNoneException( - "The content of the database initialization configuration " - "file cannot be empty") + "The content of the database initialization configuration " + "file cannot be empty ") init_database_date.sort( key=lambda x: x['priority'], reverse=False) return jsonify( -- Gitee From 71f32d58da1fcb6647e5038de3b78484bf10c977 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Fri, 3 Jul 2020 15:14:02 +0800 Subject: [PATCH 55/72] solve the Defect scan and the Standard scan result --- .../application/apps/package/view.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index e6b8f529..7a560af7 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -55,7 +55,7 @@ class Packages(Resource): return: json file contain package's info Exception: - Changelog: + Changelog: ''' # Get verification parameters schema = PackagesSchema() @@ -99,7 +99,7 @@ class Packages(Resource): class SinglePack(Resource): ''' description: single package management - Restful API: get、put + Restful API: get, put ChangeLog: ''' @@ -227,7 +227,7 @@ class InstallDepend(Resource): querying in one or more databases) input: binaryName - dbPreority:the array for database preority + dbPreority: the array for database preority return: resultDict{ binary_name: //binary package name @@ -304,8 +304,8 @@ class BuildDepend(Resource): build depend package's install depend (support querying in one or more databases) input: - sourceName : - dbPreority:the array for database preority + sourceName: + dbPreority: the array for database preority return: resultList[ restult[ @@ -376,7 +376,7 @@ class SelfDepend(Resource): packageType: source/binary selfBuild :0/1 withSubpack: 0/1 - dbPreority:the array for database preority + dbPreority: the array for database preority return: resultList[ restult[ @@ -447,7 +447,7 @@ class BeDepend(Resource): changeLog: ''' - def post(self, *args, **kwargs): + def post(self): ''' description: Query a package's all dependencies including be installed and built depend @@ -520,8 +520,8 @@ class Repodatas(Resource): file_context.read(), Loader=yaml.FullLoader) if init_database_date is None: raise ContentNoneException( - "The content of the database initialization configuration " - "file cannot be empty") + "The content of the database initialization configuration " + "file cannot be empty ") init_database_date.sort( key=lambda x: x['priority'], reverse=False) return jsonify( -- Gitee From da89bc1bf6e4cc34cc061172d390255d4fad857b Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Fri, 3 Jul 2020 17:41:25 +0800 Subject: [PATCH 56/72] =?UTF-8?q?=20=E8=A7=A3=E5=86=B3=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E6=8A=A5=E5=91=8A=E7=9A=84Attribute=20'=5F?= =?UTF-8?q?=5Ffile=5Fhandler'=20defined=20outside=20=5F=5Finit=5F=5F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packageship/packageship/libs/log/loghelper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packageship/packageship/libs/log/loghelper.py b/packageship/packageship/libs/log/loghelper.py index d5efe129..8a237014 100644 --- a/packageship/packageship/libs/log/loghelper.py +++ b/packageship/packageship/libs/log/loghelper.py @@ -53,6 +53,7 @@ class Log(): def __init__(self, name=__name__, path=None): self.__name = name self.__path = path + self.__file_handler = None if self.__path is None: self.__path = READCONFIG.get_system('log_path') log_name = READCONFIG.get_config('LOG', 'log_name') -- Gitee From 130855a9e1c689cb258732a6e50118ab904a127f Mon Sep 17 00:00:00 2001 From: solarhu Date: Sat, 4 Jul 2020 11:52:24 +0800 Subject: [PATCH 57/72] =?UTF-8?q?!28=20=E4=BF=AE=E6=94=B9=E5=AE=89?= =?UTF-8?q?=E8=A3=85spec=E9=97=AE=E9=A2=98=20*=20modify=20build=20error=20?= =?UTF-8?q?*=20add=20chenge=20log=20and=20modify=20release=20*=20Modify=20?= =?UTF-8?q?requires=20python3.7=20to=20python3.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packageship/pkgship.spec | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packageship/pkgship.spec b/packageship/pkgship.spec index 9b48b7ad..a48636ab 100644 --- a/packageship/pkgship.spec +++ b/packageship/pkgship.spec @@ -1,14 +1,14 @@ Name: pkgship Version: 1.0 -Release: 1 +Release: 2 Summary: Pkgship implements rpm package dependence ,maintainer, patch query and so no. License: Mulan 2.0 URL: https://gitee.com/openeuler/openEuler-Advisor -Source0: https://gitee.com/openeuler/openEuler-Advisor/pkgship-%{version}.tar +Source0: https://gitee.com/openeuler/openEuler-Advisor/pkgship-%{version}.tar.gz BuildArch: noarch -Requires: python3-pip python3-flask-restful python3-flask python3.7 python3-pyyaml +Requires: python3-pip python3-flask-restful python3-flask python3 python3-pyyaml Requires: python3-sqlalchemy python3-prettytable python3-requests #Requires: pyinstaller python3-flask-session python3-flask-script marshmallow uwsig @@ -30,13 +30,21 @@ Pkgship implements rpm package dependence ,maintainer, patch query and so no. %post #build cli bin -if [ -f "/usr/bin/pkgship" ];then +if [ -f "/usr/bin/pkgship" ]; then rm -rf /usr/bin/pkgship fi cd %{python3_sitelib}/packageship/ -/usr/local/bin/pyinstaller -F pkgship.py +if [ -f "/usr/bin/pyinstaller" ]; then + /usr/bin/pyinstaller -F pkgship.py +elif [ -f "/usr/local/bin/pyinstaller" ]; then + /usr/local/bin/pyinstaller -F pkgship.py +else + echo "pkship install fail,there is no pyinstaller!" + exit +fi + sed -i "s/hiddenimports\=\[\]/hiddenimports\=\['pkg_resources.py2_warn'\]/g" pkgship.spec /usr/local/bin/pyinstaller pkgship.spec cp dist/pkgship /usr/bin/ @@ -53,6 +61,9 @@ rm -rf %{python3_sitelib}/packageship/build %{python3_sitelib}/packageship/dist %changelog +* Sat JUL 4 2020 Yiru Wang - 1.0-2 +- cheange requires python3.7 to python3,add check pyinstaller file. + * Tue JUN 30 2020 Yiru Wang - 1.0-1 - add pkgshipd file -- Gitee From 24e7977e45a53840869b42b3ce4b085e9c1c0c57 Mon Sep 17 00:00:00 2001 From: jiang Date: Mon, 6 Jul 2020 09:25:48 +0800 Subject: [PATCH 58/72] The problem of modifying ASCI in Example --- .../example/annotation_specifications.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packageship/example/annotation_specifications.py b/packageship/example/annotation_specifications.py index 51a2368a..169f1cc3 100644 --- a/packageship/example/annotation_specifications.py +++ b/packageship/example/annotation_specifications.py @@ -2,15 +2,19 @@ description: Function and class annotation specifications in the project functions: test """ -# py文件中,如果全部为函数,顶部信息格式如上,description填写描述信息,functions填写函数名称 +# In the PY file, if all are functions, the format of the top information is as above, +# the description information is filled in, and the function name is filled in functions # Args: -# 列出每个参数的名字, 并在名字后使用一个冒号和一个空格, -# 分隔对该参数的描述.如果描述太长超过了单行80字符,使用2或者4个空格的悬挂缩进(与文件其他部分保持一致). -# 描述应该包括所需的类型和含义. +# List the name of each parameter with a colon and a space after the name, +# Separate the description of this parameter. +# If the description is too long than 80 characters in a single line, +# use a hanging indent of 2 or 4 spaces (consistent with the rest of the file) +# The description should include the type and meaning required # Returns: -# 描述返回值的类型和语义. 如果函数返回None, 这一部分可以省略. +# Describes the type and semantics of the return value. If the function returns none, +# this part can be omitted # Raises: -# 可能产生的异常 +# Possible anomalies def test(name, age): @@ -31,13 +35,14 @@ def test(name, age): # description: Function and class annotation specifications in the project # class: SampleClass -# py文件中,如果全部为类,顶部信息格式如上,description填写描述信息,class填写类名称,用 三引号,不用# -# 类应该在其定义下有一个用于描述该类的文档字符串. -# 如果你的类有公共属性(Attributes), -# 那么文档中应该有一个属性(Attributes)段. -# 并且应该遵守和函数参数相同的格式. - - +# In the PY file, if all are classes, the top information format is as above, +# description fills in the description information, class fills in the class name, +# uses three quotation marks, does not need# +# Class should have a document string under its definition that describes +# the class +# If your class has attributes, +# Then there should be an attribute section in the document +# And it should follow the same format as function parameters class SampleClass(): """ Summary of class here. -- Gitee From 9a7b994ff575c6e8545558b4519a8cc914627d0a Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Wed, 8 Jul 2020 09:45:01 +0800 Subject: [PATCH 59/72] set interpreter to python3 and solve the standard scanning problem --- packageship/packageship/pkgship.py | 201 +++++++++++++++-------------- 1 file changed, 101 insertions(+), 100 deletions(-) diff --git a/packageship/packageship/pkgship.py b/packageship/packageship/pkgship.py index c22a5409..c5daea4f 100644 --- a/packageship/packageship/pkgship.py +++ b/packageship/packageship/pkgship.py @@ -1,6 +1,7 @@ -''' +#!/usr/bin/python3 +""" Entry method for custom commands -''' +""" import os import json try: @@ -26,9 +27,9 @@ DB_NAME = 0 def main(): - ''' + """ command entry function - ''' + """ try: packship_cmd = PkgshipCommand() packship_cmd.parser_args() @@ -38,9 +39,9 @@ def main(): class BaseCommand(): - ''' + """ Basic attributes used for command invocation - ''' + """ def __init__(self): self._read_config = ReadConfig() @@ -54,9 +55,9 @@ class BaseCommand(): self.load_write_host() def load_write_host(self): - ''' + """ Address to load write permission - ''' + """ wirte_port = self._read_config.get_system('write_port') write_ip = self._read_config.get_system('write_ip_addr') @@ -66,9 +67,9 @@ class BaseCommand(): setattr(self, 'write_host', _write_host) def load_read_host(self): - ''' + """ Address to load write permission - ''' + """ read_port = self._read_config.get_system('query_port') read_ip = self._read_config.get_system('query_ip_addr') @@ -79,9 +80,9 @@ class BaseCommand(): class PkgshipCommand(BaseCommand): - ''' + """ PKG package command line - ''' + """ parser = argparse.ArgumentParser( description='package related dependency management') subparsers = parser.add_subparsers( @@ -99,15 +100,15 @@ class PkgshipCommand(BaseCommand): @staticmethod def register_command(command): - ''' + """ Register command - ''' + """ command.register() def register(self): - ''' + """ Command line parameter injection - ''' + """ for command_params in self.params: self.parse.add_argument( # pylint: disable=E1101 command_params[0], @@ -117,9 +118,9 @@ class PkgshipCommand(BaseCommand): @classmethod def parser_args(cls): - ''' + """ Command parsing - ''' + """ cls.register_command(RemoveCommand()) cls.register_command(InitDatabaseCommand()) cls.register_command(UpdateDatabaseCommand()) @@ -137,9 +138,9 @@ class PkgshipCommand(BaseCommand): print('command error') def parse_package(self, response_data): - ''' + """ Parse the corresponding data of the package - ''' + """ if response_data.get('code') == ResponseCode.SUCCESS: package_all = response_data.get('data') if isinstance(package_all, list): @@ -151,9 +152,9 @@ class PkgshipCommand(BaseCommand): print(response_data.get('msg')) def parse_depend_package(self, response_data): - ''' + """ Parse the corresponding data of the package - ''' + """ bin_package_count = 0 src_package_count = 0 if response_data.get('code') == ResponseCode.SUCCESS: @@ -198,9 +199,9 @@ class PkgshipCommand(BaseCommand): return statistics_table def print_(self, content=None, character='=', dividing_line=False): - ''' + """ Output formatted characters - ''' + """ # Get the current width of the console if dividing_line: @@ -212,9 +213,9 @@ class PkgshipCommand(BaseCommand): @staticmethod def create_table(title): - ''' + """ Create printed forms - ''' + """ table = PrettyTable(title) # table.set_style(prettytable.PLAIN_COLUMNS) table.align = 'l' @@ -225,9 +226,9 @@ class PkgshipCommand(BaseCommand): return table def statistics_table(self, bin_package_count, src_package_count): - ''' + """ Generate data for total statistical tables - ''' + """ statistics_table = self.create_table(['', 'binary', 'source']) statistics_table.add_row( ['self depend sum', bin_package_count, src_package_count]) @@ -240,9 +241,9 @@ class PkgshipCommand(BaseCommand): @staticmethod def http_error(response): - ''' + """ Log error messages for http - ''' + """ try: print(response.raise_for_status()) except HTTPError as http_error: @@ -252,9 +253,9 @@ class PkgshipCommand(BaseCommand): class RemoveCommand(PkgshipCommand): - ''' + """ Delete database command - ''' + """ def __init__(self): super(RemoveCommand, self).__init__() @@ -263,16 +264,16 @@ class RemoveCommand(PkgshipCommand): self.params = [('db', 'str', 'name of the database operated', '')] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(RemoveCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ if params.db is None: print('No database specified for deletion') else: @@ -296,9 +297,9 @@ class RemoveCommand(PkgshipCommand): class InitDatabaseCommand(PkgshipCommand): - ''' + """ Initialize database command - ''' + """ def __init__(self): super(InitDatabaseCommand, self).__init__() @@ -308,16 +309,16 @@ class InitDatabaseCommand(PkgshipCommand): ('-filepath', 'str', 'name of the database operated', '')] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(InitDatabaseCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ file_path = params.filepath try: response = requests.post(self.write_host + @@ -339,9 +340,9 @@ class InitDatabaseCommand(PkgshipCommand): class UpdateDatabaseCommand(PkgshipCommand): - ''' + """ update database command - ''' + """ def __init__(self): super(UpdateDatabaseCommand, self).__init__() @@ -351,23 +352,23 @@ class UpdateDatabaseCommand(PkgshipCommand): self.params = [('db', 'str', 'name of the database operated', '')] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(UpdateDatabaseCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ pass # pylint: disable= W0107 class AllPackageCommand(PkgshipCommand): - ''' + """ get all package commands - ''' + """ def __init__(self): super(AllPackageCommand, self).__init__() @@ -379,16 +380,16 @@ class AllPackageCommand(PkgshipCommand): self.params = [('-db', 'str', 'name of the database operated', '')] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(AllPackageCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.read_host + \ '/packages?dbName={dbName}'.format(dbName=params.db) try: @@ -407,9 +408,9 @@ class AllPackageCommand(PkgshipCommand): class UpdatePackageCommand(PkgshipCommand): - ''' + """ update package data - ''' + """ def __init__(self): super(UpdatePackageCommand, self).__init__() @@ -424,16 +425,16 @@ class UpdatePackageCommand(PkgshipCommand): ] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(UpdatePackageCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.write_host + '/packages/findByPackName' try: response = requests.put( @@ -458,9 +459,9 @@ class UpdatePackageCommand(PkgshipCommand): class BuildDepCommand(PkgshipCommand): - ''' + """ query the compilation dependencies of the specified package - ''' + """ def __init__(self): super(BuildDepCommand, self).__init__() @@ -476,9 +477,9 @@ class BuildDepCommand(PkgshipCommand): ] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(BuildDepCommand, self).register() # collection parameters @@ -488,9 +489,9 @@ class BuildDepCommand(PkgshipCommand): self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.read_host + '/packages/findBuildDepend' try: response = requests.post( @@ -515,9 +516,9 @@ class BuildDepCommand(PkgshipCommand): class InstallDepCommand(PkgshipCommand): - ''' + """ query the installation dependencies of the specified package - ''' + """ def __init__(self): super(InstallDepCommand, self).__init__() @@ -533,9 +534,9 @@ class InstallDepCommand(PkgshipCommand): ] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(InstallDepCommand, self).register() # collection parameters @@ -545,9 +546,9 @@ class InstallDepCommand(PkgshipCommand): self.parse.set_defaults(func=self.do_command) def parse_package(self, response_data): - ''' + """ Parse the corresponding data of the package - ''' + """ if getattr(self, 'statistics'): setattr(self, 'statistics', dict()) bin_package_count = 0 @@ -593,9 +594,9 @@ class InstallDepCommand(PkgshipCommand): return statistics_table def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.read_host + '/packages/findInstallDepend' try: response = requests.post(_url, data=json.dumps( @@ -621,9 +622,9 @@ class InstallDepCommand(PkgshipCommand): class SelfBuildCommand(PkgshipCommand): - ''' + """ self-compiled dependency query - ''' + """ def __init__(self): super(SelfBuildCommand, self).__init__() @@ -647,9 +648,9 @@ class SelfBuildCommand(PkgshipCommand): ] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(SelfBuildCommand, self).register() # collection parameters @@ -659,9 +660,9 @@ class SelfBuildCommand(PkgshipCommand): self.parse.set_defaults(func=self.do_command) def _parse_bin_package(self, bin_packages): - ''' + """ Parsing binary result data - ''' + """ bin_package_count = 0 if bin_packages: for bin_package, package_depend in bin_packages.items(): @@ -688,9 +689,9 @@ class SelfBuildCommand(PkgshipCommand): return bin_package_count def _parse_src_package(self, src_apckages): - ''' + """ Source package data analysis - ''' + """ src_package_count = 0 if src_apckages: for src_package, package_depend in src_apckages.items(): @@ -716,9 +717,9 @@ class SelfBuildCommand(PkgshipCommand): return src_package_count def parse_package(self, response_data): - ''' + """ Parse the corresponding data of the package - ''' + """ if getattr(self, 'statistics'): setattr(self, 'statistics', dict()) bin_package_count = 0 @@ -744,9 +745,9 @@ class SelfBuildCommand(PkgshipCommand): return statistics_table def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.read_host + '/packages/findSelfDepend' try: response = requests.post(_url, @@ -778,9 +779,9 @@ class SelfBuildCommand(PkgshipCommand): class BeDependCommand(PkgshipCommand): - ''' + """ dependent query - ''' + """ def __init__(self): super(BeDependCommand, self).__init__() @@ -794,16 +795,16 @@ class BeDependCommand(PkgshipCommand): ] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(BeDependCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.read_host + '/packages/findBeDepend' try: response = requests.post(_url, data=json.dumps( @@ -831,9 +832,9 @@ class BeDependCommand(PkgshipCommand): class SingleCommand(PkgshipCommand): - ''' + """ query single package information - ''' + """ def __init__(self): super(SingleCommand, self).__init__() @@ -846,16 +847,16 @@ class SingleCommand(PkgshipCommand): ] def register(self): - ''' + """ Command line parameter injection - ''' + """ super(SingleCommand, self).register() self.parse.set_defaults(func=self.do_command) def parse_package(self, response_data): - ''' + """ Parse the corresponding data of the package - ''' + """ show_field_name = ('sourceName', 'dbname', 'version', 'license', 'maintainer', 'maintainlevel') print_contents = [] @@ -878,9 +879,9 @@ class SingleCommand(PkgshipCommand): self.print_(content=content) def do_command(self, params): - ''' + """ Action to execute command - ''' + """ _url = self.read_host + \ '/packages/findByPackName?dbName={db_name}&sourceName={packagename}' \ .format(db_name=params.db, packagename=params.packagename) -- Gitee From 322be7a41d1d27c9e3dabe1fa44c3d529dde2066 Mon Sep 17 00:00:00 2001 From: gongzt Date: Wed, 8 Jul 2020 09:55:51 +0800 Subject: [PATCH 60/72] Added Python version description to the file header --- packageship/packageship/application/__init__.py | 1 + packageship/packageship/application/app_global.py | 1 + packageship/packageship/application/initsystem/data_import.py | 1 + packageship/packageship/application/initsystem/datamerge.py | 1 + packageship/packageship/application/models/package.py | 1 + packageship/packageship/application/models/temporarydb.py | 1 + packageship/packageship/application/settings.py | 1 + packageship/packageship/libs/configutils/readconfig.py | 1 + packageship/packageship/libs/dbutils/sqlalchemy_helper.py | 1 + packageship/packageship/libs/exception/ext.py | 1 + packageship/packageship/libs/log/loghelper.py | 1 + packageship/packageship/manage.py | 2 +- packageship/packageship/pkgship.py | 1 + packageship/packageship/selfpkg.py | 1 + packageship/packageship/system_config.py | 1 + 15 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packageship/packageship/application/__init__.py b/packageship/packageship/application/__init__.py index e991c560..23cc8137 100644 --- a/packageship/packageship/application/__init__.py +++ b/packageship/packageship/application/__init__.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Initial operation and configuration of the flask project ''' diff --git a/packageship/packageship/application/app_global.py b/packageship/packageship/application/app_global.py index 2684a6fc..726b1b96 100644 --- a/packageship/packageship/application/app_global.py +++ b/packageship/packageship/application/app_global.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Interception before request ''' diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index 84262557..960d3bcd 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Initialization of data import Import the data in the sqlite database into the mysql database diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index 1812fb55..165c24e9 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Integration of multiple sqlite file data, including reading sqlite database and inserting data diff --git a/packageship/packageship/application/models/package.py b/packageship/packageship/application/models/package.py index ba65a150..95e62a6a 100644 --- a/packageship/packageship/application/models/package.py +++ b/packageship/packageship/application/models/package.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Database entity model mapping ''' diff --git a/packageship/packageship/application/models/temporarydb.py b/packageship/packageship/application/models/temporarydb.py index 9df8c3db..79d3c7ec 100644 --- a/packageship/packageship/application/models/temporarydb.py +++ b/packageship/packageship/application/models/temporarydb.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Database entity model mapping ''' diff --git a/packageship/packageship/application/settings.py b/packageship/packageship/application/settings.py index 59462373..41fac74f 100644 --- a/packageship/packageship/application/settings.py +++ b/packageship/packageship/application/settings.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Basic configuration of flask framework ''' diff --git a/packageship/packageship/libs/configutils/readconfig.py b/packageship/packageship/libs/configutils/readconfig.py index 8e2aeeae..e2ec3a46 100644 --- a/packageship/packageship/libs/configutils/readconfig.py +++ b/packageship/packageship/libs/configutils/readconfig.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Read the base class of the configuration file in the system which mainly includes obtaining specific node values diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index 56940532..b94c16ed 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Simple encapsulation of sqlalchemy orm framework operation database diff --git a/packageship/packageship/libs/exception/ext.py b/packageship/packageship/libs/exception/ext.py index 12a96ef6..64f954c0 100644 --- a/packageship/packageship/libs/exception/ext.py +++ b/packageship/packageship/libs/exception/ext.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' System exception information ''' diff --git a/packageship/packageship/libs/log/loghelper.py b/packageship/packageship/libs/log/loghelper.py index 8a237014..91541477 100644 --- a/packageship/packageship/libs/log/loghelper.py +++ b/packageship/packageship/libs/log/loghelper.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Logging related ''' diff --git a/packageship/packageship/manage.py b/packageship/packageship/manage.py index d314ea68..1cef1912 100644 --- a/packageship/packageship/manage.py +++ b/packageship/packageship/manage.py @@ -1,8 +1,8 @@ +#!/usr/bin/python3 ''' Entry for project initialization and service startupc ''' import os -from flask_script import Manager from packageship.libs.exception import Error try: from packageship.system_config import SYS_CONFIG_PATH diff --git a/packageship/packageship/pkgship.py b/packageship/packageship/pkgship.py index 921e20e0..9a97177e 100644 --- a/packageship/packageship/pkgship.py +++ b/packageship/packageship/pkgship.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Entry method for custom commands ''' diff --git a/packageship/packageship/selfpkg.py b/packageship/packageship/selfpkg.py index 31d9add2..7a14a9a6 100644 --- a/packageship/packageship/selfpkg.py +++ b/packageship/packageship/selfpkg.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' Entry for project initialization and service startupc ''' diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py index e5a0d53d..df2dc142 100644 --- a/packageship/packageship/system_config.py +++ b/packageship/packageship/system_config.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 ''' System-level file configuration, mainly configure the address of the operating environment, commonly used variables, etc. -- Gitee From fd1734307c64a0b931e465ab3af5eed3e37abdd1 Mon Sep 17 00:00:00 2001 From: gongzt Date: Wed, 8 Jul 2020 10:11:07 +0800 Subject: [PATCH 61/72] Solve the problem of filter deep traversal efficiency --- .../packageship/application/initsystem/datamerge.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index 165c24e9..553a5b7d 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -184,14 +184,17 @@ class MergeData(): if src_package_name: # Find the maintainer information of the current data maintenance_infos = self.mainter_infos.get(src_package_name) - maintenance = None + maintenance = [] + version = src_package_item.get('version') if self.mainter_infos.get(src_package_name): - maintenance = list(filter(lambda x: x.get( - 'version') == src_package_item.get('version'), maintenance_infos)) + for maintenance_item in maintenance_infos: + if maintenance_item.get('version') == version: + maintenance.append(maintenance_item) + self.src_package_datas.append( { "name": src_package_item.get('name'), - "version": src_package_item.get('version'), + "version": version, "rpm_license": src_package_item.get('rpm_license'), "url": src_package_item.get('url'), "pkgKey": src_package_item.get('pkgKey'), -- Gitee From eeb2751589db0aa14f337f273519789c45933931 Mon Sep 17 00:00:00 2001 From: MementoMori <1003350679@qq.com> Date: Wed, 8 Jul 2020 10:28:48 +0800 Subject: [PATCH 62/72] Add Annotate --- .../application/initsystem/data_import.py | 441 ++++++++---- .../application/initsystem/datamerge.py | 183 +++-- .../libs/dbutils/sqlalchemy_helper.py | 143 ++-- packageship/packageship/pkgship.py | 643 +++++++++++++----- 4 files changed, 1022 insertions(+), 388 deletions(-) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index e7dafde2..aaed2d8e 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -1,7 +1,9 @@ -''' - Initialization of data import -    Import the data in the sqlite database into the mysql database -''' +#!/usr/bin/python3 +""" +Description: Initialization of data import + Import the data in the sqlite database into the mysql database +Class: InitDataBase,MysqlDatabaseOperations,SqliteDatabaseOperations +""" import os import pathlib import yaml @@ -27,17 +29,20 @@ LOGGER = Log(__name__) class InitDataBase(): - ''' - Database initialization, generate multiple databases and data based on configuration files - ''' + """ + Description: Database initialization, generate multiple databases and data based on configuration files + Attributes: + config_file_path: configuration file path + config_file_datas: initialize the configuration content of the database + db_type: type of database + """ def __init__(self, config_file_path=None): - ''' - parameter: - config_file_path:The path of the configuration file that needs to initialize - the database in the project - ''' - + """ + Description: Class instance initialization + Args: + config_file_path: Configuration file path + """ self.config_file_path = config_file_path if self.config_file_path: @@ -56,11 +61,17 @@ class InitDataBase(): raise Exception('database type configuration error') def __read_config_file(self): - ''' - functional description: - Read the contents of the configuration file load each node data in the yaml - configuration file as a list to return - ''' + """ + Description: Read the contents of the configuration file load each node data in the yaml configuration file as + a list to return + Args: + + Returns: + Initialize the contents of the database configuration file + Raises: + FileNotFoundError: The specified file does not exist + TypeError: Wrong type of data + """ if not os.path.exists(self.config_file_path): raise FileNotFoundError( @@ -80,9 +91,15 @@ class InitDataBase(): return init_database_config def init_data(self): - ''' - functional description:Initialization of the database - ''' + """ + Description: Initialization of the database + Args: + + Returns: + + Raises: + IOError: An error occurred while deleting the database information file + """ if getattr(self, 'config_file_datas', None) is None or \ self.config_file_datas is None: raise ContentNoneException('Initialization file content is empty') @@ -116,9 +133,16 @@ class InitDataBase(): self._init_data(database) def _create_database(self, database): - ''' - create related databases - ''' + """ + Description: create related databases + Args: + database: Initialize the configuration content of the database + Returns: + The generated mysql database or sqlite database + Raises: + SQLAlchemyError: Abnormal database operation + """ + db_name = database.get('dbname') self._sqlite_db = SqliteDatabaseOperations(db_name=db_name) @@ -135,11 +159,20 @@ class InitDataBase(): return sqltedb_file def _init_data(self, database): - ''' - functional description:data initialization operation - parameter: - database:database configuration information - ''' + """ + Description: data initialization operation + Args: + database: Initialize the configuration content of the database + Returns: + + Raises: + ContentNoneException: Exception with empty content + TypeError: Data type error + SQLAlchemyError: Abnormal database operation + DataMergeException: Error in data integration + IOError: An error occurred while deleting the database information file + """ + try: db_file = None # 1. create a database and related tables in the database @@ -160,7 +193,7 @@ class InitDataBase(): src_package_paths, bin_package_paths) # 4. dependencies between combined data self.data_relationship(db_file) - # # 5. save data + # 5. save data self.save_data(db_name) except (SQLAlchemyError, ContentNoneException, @@ -192,9 +225,16 @@ class InitDataBase(): LOGGER.logger.error(error_msg) def _src_package_relation(self, src_package_data): - ''' - Mapping of data relations of source packages - ''' + """ + Description: Mapping of data relations of source packages + Args: + src_package_data: Source package data + Returns: + + Raises: + + """ + _src_package_name = src_package_data.name _src_package = { "name": src_package_data.name, @@ -219,9 +259,16 @@ class InitDataBase(): self._src_package_names[src_package_data.pkgKey] = _src_package_name def _src_requires_relation(self, src_requires_data): - ''' - Source package dependent package data relationship mapping - ''' + """ + Description: Source package dependent package data relationship mapping + Args: + src_requires_data: Source package dependent package data + Returns: + + Raises: + + """ + _src_package_name = self._src_package_names.get( src_requires_data.pkgKey) if _src_package_name: @@ -232,9 +279,16 @@ class InitDataBase(): }) def _bin_package_relation(self, bin_package_data): - ''' - Binary package relationship mapping problem - ''' + """ + Description: Binary package relationship mapping problem + Args: + bin_package_data: Binary package data + Returns: + + Raises: + + """ + _bin_pkg_key = bin_package_data.pkgKey self._bin_package_name[bin_package_data.name] = _bin_pkg_key @@ -257,9 +311,16 @@ class InitDataBase(): self._bin_package_dicts[src_package_name].append(_bin_package) def _bin_requires_relation(self, bin_requires_data): - ''' - Binary package dependency package relationship mapping problem - ''' + """ + Description: Binary package dependency package relationship mapping problem + Args: + bin_requires_data: Binary package dependency package data + Returns: + + Raises: + + """ + _bin_pkg_key = bin_requires_data.pkgKey if _bin_pkg_key: if _bin_pkg_key not in self._bin_requires_dicts: @@ -270,9 +331,16 @@ class InitDataBase(): }) def _bin_provides_relation(self, bin_provides_data): - ''' - Binary package provided by the relationship mapping problem - ''' + """ + Description: Binary package provided by the relationship mapping problem + Args: + bin_provides_data: Component data provided by the binary package + Returns: + + Raises: + + """ + _bin_pkg_key = bin_provides_data.pkgKey if _bin_pkg_key: if _bin_pkg_key not in self._bin_provides_dicts: @@ -282,9 +350,16 @@ class InitDataBase(): }) def data_relationship(self, db_file): - ''' - dependencies between combined data - ''' + """ + Description: dependencies between combined data + Args: + db_file: Temporary database file + Returns: + + Raises: + Error information + """ + self._bin_provides_dicts = dict() self._bin_requires_dicts = dict() self._bin_package_name = dict() @@ -320,9 +395,16 @@ class InitDataBase(): LOGGER.logger.error(error_msg) def file_merge(self, src_package_paths, bin_package_paths): - ''' - integration of multiple data files - ''' + """ + Description: integration of multiple data files + Args: + src_package_paths: Source package database file + bin_package_paths: Binary package database file + Returns: + Path of the generated temporary database file + Raises: + DataMergeException: Abnormal data integration + """ _db_file = os.path.join( self._sqlite_db.database_file_folder, 'temporary_database') @@ -361,14 +443,15 @@ class InitDataBase(): return _db_file def __exists_repeat_database(self): - ''' - functional description:Determine if the same database name exists - parameter: - return value: - exception description: - modify record: - ''' + """ + Description: Determine if the same database name exists + Args: + Returns: + True if there are duplicate databases, false otherwise + Raises: + + """ db_names = [name.get('dbname') for name in self.config_file_datas] @@ -378,9 +461,15 @@ class InitDataBase(): return False def _save_bin_package(self, src_packages): - ''' - Save binary package data - ''' + """ + Description: Save binary package data + Args: + src_packages: Source package data + Returns: + Binary package data + Raises: + + """ bin_packages = [] for package_data in src_packages: try: @@ -428,9 +517,15 @@ class InitDataBase(): return bin_packages def _save_bin_provides(self, bin_packages): - ''' - Save package data provided by binary - ''' + """ + Description: Save package data provided by binary + Args: + bin_packages: Binary package data + Returns: + Package data provided by binary + Raises: + + """ bin_provides_list = [] for bin_pack_entity in bin_packages: @@ -459,9 +554,16 @@ class InitDataBase(): return bin_provides_list def save_data(self, db_name): - ''' - save related package data - ''' + """ + Description: save related package data + Args: + db_name: The name of the database + Returns: + + Raises: + + """ + with DBHelper(db_name=db_name) as data_base: # Add source package data data_base.batch_add( @@ -490,13 +592,17 @@ class InitDataBase(): @staticmethod def __updata_settings_file(**Kwargs): - ''' - update some configuration files related to the database in the system - parameter: - **Kwargs:data related to configuration file nodes - database_name: Name database - priority: priority - ''' + """ + Description: update some configuration files related to the database in the system + Args: + **Kwargs: data related to configuration file nodes + database_name: Name database + Returns: + + Raises: + FileNotFoundError: The specified file was not found + IOError: File or network operation io abnormal + """ try: if not os.path.exists(DATABASE_FILE_INFO): pathlib.Path(DATABASE_FILE_INFO).touch() @@ -515,13 +621,16 @@ class InitDataBase(): @staticmethod def delete_settings_file(): - ''' - functional description:Delete the configuration file of the database - parameter: - return value: - exception description: - modify record: - ''' + """ + Description: Delete the configuration file of the database + Args: + + Returns: + True if the deletion is successful, otherwise false + Raises: + IOError: File or network operation io abnormal + """ + try: if os.path.exists(DATABASE_FILE_INFO): os.remove(DATABASE_FILE_INFO) @@ -532,9 +641,15 @@ class InitDataBase(): return True def delete_db(self, db_name): - ''' - Delete the database - ''' + """ + Description: elete the database + Args: + db_name: The name of the database + Returns: + + Raises: + IOError: File or network operation io abnormal + """ if self.db_type == 'mysql': del_result = MysqlDatabaseOperations.drop_database(db_name) else: @@ -560,12 +675,20 @@ class InitDataBase(): class MysqlDatabaseOperations(): - ''' - Related to database operations, creating databases, creating tables - ''' + """ + Description: Related to database operations, creating databases, creating tables + Attributes: + db_name: The name of the database + create_database_sql: SQL statement to create a database + drop_database_sql: Delete the SQL statement of the database + """ def __init__(self, db_name): - + """ + Description: Class instance initialization + Args: + db_name: Database name + """ self.db_name = db_name self.create_database_sql = ''' CREATE DATABASE if not exists `{db_name}` \ DEFAULT CHARACTER SET utf8mb4; '''.format(db_name=self.db_name) @@ -573,9 +696,15 @@ class MysqlDatabaseOperations(): db_name=self.db_name) def create_database(self): - ''' - functional description:create a database - ''' + """ + Description: create a database + Args: + + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database + """ with DBHelper(db_name='mysql') as data_base: @@ -592,14 +721,15 @@ class MysqlDatabaseOperations(): @classmethod def drop_database(cls, db_name): - ''' - functional description:Delete the database according to the specified name - parameter: - :db_name:The name of the database - return value: - exception description: - modify record: - ''' + """ + Description: Delete the database according to the specified name + Args: + db_name: The name of the database to be deleted + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database + """ if db_name is None: raise IOError( "The name of the database to be deleted cannot be empty") @@ -615,9 +745,15 @@ class MysqlDatabaseOperations(): return True def __create_tables(self): - ''' - Create the specified data table - ''' + """ + Description: Create the specified data table + Args: + + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database + """ try: with DBHelper(db_name=self.db_name) as database: tables = ['src_pack', 'bin_pack', 'pack_provides', @@ -631,9 +767,15 @@ class MysqlDatabaseOperations(): return True def create_datum_database(self): - ''' - Create a benchmark database to save the maintainer's information - ''' + """ + Description: Create a benchmark database to save the maintainer's information + Args: + + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database + """ with DBHelper(db_name='mysql') as data_base: # create database try: @@ -646,9 +788,16 @@ class MysqlDatabaseOperations(): return self.__create_datum_tables() def __create_datum_tables(self): - ''' - Create a data table of maintainer information - ''' + """ + Description: Create a data table of maintainer information + rgs: + + Returns: + True if successful, otherwise false + Raises: + SQLAlchemyError: An exception occurred while creating the database + Error: Error information + """ try: with DBHelper(db_name=self.db_name) as database: tables = ['maintenance_info'] @@ -665,12 +814,20 @@ class MysqlDatabaseOperations(): class SqliteDatabaseOperations(): - ''' - sqlite database related operations - ''' + """ + Description: sqlite database related operations + Attributes: + db_name: Name database + database_file_folder: Database folder path + """ def __init__(self, db_name, **kwargs): - + """ + Description: Class instance initialization + Args: + db_name: Database name + kwargs: data related to configuration file nodes + """ self.db_name = db_name self._read_config = ReadConfig() if getattr(kwargs, 'database_path', None) is None: @@ -678,10 +835,15 @@ class SqliteDatabaseOperations(): else: self.database_file_folder = kwargs.get('database_path') - def database_file_path(self): - ''' - Database file path - ''' + def _database_file_path(self): + """ + Description: Database file path + Args: + Returns: + + Raises: + IOError: File or network operation io abnormal + """ self.database_file_folder = self._read_config.get_system( 'data_base_path') if not self.database_file_folder: @@ -695,16 +857,24 @@ class SqliteDatabaseOperations(): self.database_file_folder = None def create_sqlite_database(self): - ''' - create sqlite database and table - ''' + """ + Description: create sqlite database and table + Args: + + Returns: + After successful generation, return the database file address, + otherwise return none + Raises: + FileNotFoundError: The specified folder path does not exist + SQLAlchemyError: An error occurred while generating the database + """ if self.database_file_folder is None: raise FileNotFoundError('Database folder does not exist') _db_file = os.path.join( self.database_file_folder, self.db_name) - if os.path.exists(_db_file+'.db'): + if os.path.exists(_db_file + '.db'): os.remove(_db_file + '.db') # create a sqlite database @@ -720,12 +890,19 @@ class SqliteDatabaseOperations(): return _db_file def drop_database(self): - ''' - Delete the specified sqlite database - ''' + """ + Description: Delete the specified sqlite database + Args: + + Returns: + Return true after successful deletion, otherwise return false + Raises: + IOError: An io exception occurred while deleting the specified database file + """ + try: db_path = os.path.join( - self.database_file_folder, self.db_name+'.db') + self.database_file_folder, self.db_name + '.db') if os.path.exists(db_path): os.remove(db_path) except IOError as exception_msg: @@ -735,16 +912,24 @@ class SqliteDatabaseOperations(): return True def create_datum_database(self): - ''' - create sqlite database and table - ''' + """ + Description: create sqlite database and table + Args: + + Returns: + After successful generation, return the database file address, + otherwise return none + Raises: + FileNotFoundError: The specified database folder does not exist + SQLAlchemyError: An error occurred while generating the database + """ if self.database_file_folder is None: raise FileNotFoundError('Database folder does not exist') _db_file = os.path.join( self.database_file_folder, self.db_name) - if not os.path.exists(_db_file+'.db'): + if not os.path.exists(_db_file + '.db'): # create a sqlite database with DBHelper(db_name=_db_file) as database: tables = ['maintenance_info'] diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index cea7512a..687f7e08 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -1,7 +1,8 @@ -''' - Integration of multiple sqlite file data, including - reading sqlite database and inserting data -''' +#!/usr/bin/python3 +""" +Description: Integration of multiple sqlite file data, including reading sqlite database and inserting data +Class: MergeData +""" from sqlalchemy.exc import SQLAlchemyError from packageship.application.models.temporarydb import src_package from packageship.application.models.temporarydb import src_requires @@ -16,30 +17,50 @@ LOGGER = Log(__name__) class MergeData(): - ''' - Load data from sqlite database - ''' + """ + Description: Load data from sqlite database + Attributes: + db_file: Database file + db_type: Connected database type + datum_database: Base database name + """ def __init__(self, db_file): - + """ + Description: Class instance initialization + Args: + db_file: Database file + """ self.db_file = db_file self.db_type = 'sqlite:///' self.datum_database = 'maintenance.information' @staticmethod def __columns(cursor): - ''' - functional description:Returns all the column names queried by the current cursor - ''' + """ + Description: functional description:Returns all the column names queried by the current cursor + Args: + cursor: Cursor + + Returns: + The first columns + Raises: + """ return [col[0] for col in cursor.description] def get_package_data(self): - ''' - get binary package or source package data - ''' + """ + Description: get binary package or source package data + Args: + + Returns: + All source package data queried + Raises: + SQLAlchemyError: An error occurred while executing the sql statement + """ try: - with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ + with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True) \ as database: src_packages_data = database.session.execute( "select pkgKey,name,version,rpm_license,url,rpm_sourcerpm from packages") @@ -51,11 +72,17 @@ class MergeData(): return None def get_requires_data(self): - ''' - get dependent package data of binary package or source package - ''' + """ + Description: get dependent package data of binary package or source package + Args: + + Returns: + All dependent data queried + Raises: + SQLAlchemyError: An error occurred while executing the sql statement + """ try: - with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ + with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True) \ as database: requires = database.session.execute( "select pkgKey,name from requires") @@ -66,11 +93,17 @@ class MergeData(): return None def get_provides(self): - ''' - get the dependency package provided by the binary package - ''' + """ + Description: get the dependency package provided by the binary package + Args: + + Returns: + Query the component data provided by all binary packages + Raises: + SQLAlchemyError: An error occurred while executing the sql statement + """ try: - with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True)\ + with DBHelper(db_name=self.db_file, db_type=self.db_type, import_database=True) \ as database: requires = database.session.execute( "select pkgKey,name from provides") @@ -81,9 +114,15 @@ class MergeData(): return None def get_maintenance_info(self): - ''' - Obtain the information of the maintainer - ''' + """ + Description: Obtain the information of the maintainer + Args: + + Returns: + Maintainer related information + Raises: + SQLAlchemyError: An error occurred while executing the sql statement + """ try: if not hasattr(self, 'mainter_infos'): self.mainter_infos = dict() @@ -99,9 +138,17 @@ class MergeData(): LOGGER.logger.error(sql_error) def src_file_merge(self, src_package_key, db_file): - ''' - Source code related data integration - ''' + """ + Description: Source code related data integration + Args: + src_package_key: The relevant key value of the source package + db_file: Database file + Returns: + Key value after successful data combination + (0, False) or (src_package_key, True) + Raises: + SQLAlchemyError: An error occurred while executing the sql statement + """ self.get_maintenance_info() self.__compose_src_package() @@ -131,9 +178,15 @@ class MergeData(): return (src_package_key, True) def __compose_src_package(self): - ''' - Combine source package data - ''' + """ + Description: Combine source package data + Args: + + Returns: + + Raises: + + """ if getattr(self, 'src_package_datas', None) is None: self.src_package_datas = [] @@ -154,15 +207,21 @@ class MergeData(): "url": src_package_item.get('url'), "pkgKey": src_package_item.get('pkgKey'), 'maintaniner': - maintenance[0].get('maintaniner') if maintenance and len( - maintenance) > 0 else None + maintenance[0].get('maintaniner') if maintenance and len( + maintenance) > 0 else None } ) def __compose_src_rquires(self): - ''' - Combine source package dependent package data - ''' + """ + Description: Combine source package dependent package data + Args: + + Returns: + + Raises: + + """ if getattr(self, 'src_requires_dicts', None) is None: self.src_requires_dicts = dict() @@ -179,9 +238,15 @@ class MergeData(): ) def __compose_bin_package(self): - ''' - Combine binary package data - ''' + """ + Description: Combine binary package data + Args: + + Returns: + + Raises: + AttributeError + """ if getattr(self, 'bin_package_datas', None) is None: self.bin_package_datas = [] @@ -205,9 +270,14 @@ class MergeData(): ) def __compose_bin_requires(self): - ''' - Combining binary dependent package data - ''' + """ + Description: Combining binary dependent package data + Args: + + Returns: + + Raises: + """ if getattr(self, 'bin_requires_dicts', None) is None: self.bin_requires_dicts = dict() @@ -222,9 +292,15 @@ class MergeData(): }) def __compose_bin_provides(self): - ''' - Combine binary package data - ''' + """ + Description: Combine binary package data + Args: + + Returns: + + Raises: + + """ if getattr(self, 'bin_provides_dicts', None) is None: self.bin_provides_dicts = dict() @@ -239,10 +315,17 @@ class MergeData(): }) def bin_file_merge(self, bin_package_key, db_file): - ''' - Binary package related data integration - ''' - + """ + Description: Binary package related data integration + Args: + bin_package_key: Primary key of binary package + db_file: Database file + Returns: + Key value after successful data combination + (0, False) or (bin_package_key, True) + Raises: + SQLAlchemyError: An error occurred while executing the sql statement + """ self.__compose_bin_package() # binary package dependent package integration diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index 93097a61..9662b0c1 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -1,7 +1,8 @@ -''' -Simple encapsulation of sqlalchemy orm framework operation database - -''' +#!/usr/bin/python3 +""" +Description: Simple encapsulation of sqlalchemy orm framework operation database +Class: DBHelper +""" import os from sqlalchemy import create_engine from sqlalchemy import MetaData @@ -18,22 +19,34 @@ from packageship import system_config class DBHelper(): - ''' - Database connection, operation public class - ''' + """ + Description: Database connection, operation public class + Attributes: + user_name: Username + password: Password + ip_address: Ip address + port: Port + db_name: Database name + db_type: Database type + session: Session + """ # The base class inherited by the data model BASE = declarative_base() def __init__(self, user_name=None, passwrod=None, ip_address=None, # pylint: disable=R0913 port=None, db_name=None, db_type=None, **kwargs): + """ + Description: Class instance initialization + + """ self.user_name = user_name self._readconfig = ReadConfig() if self.user_name is None: self.user_name = self._readconfig.get_database('user_name') - self.passwrod = passwrod - if self.passwrod is None: - self.passwrod = self._readconfig.get_database('password') + self.password = password + if self.password is None: + self.password = self._readconfig.get_database('password') self.ip_address = ip_address @@ -67,9 +80,15 @@ class DBHelper(): self.session = None def _create_engine(self): - ''' - Create a database connection object - ''' + """ + Description: Create a database connection object + Args: + + Returns: + Raises: + DisconnectionError: A disconnect is detected on a raw DB-API connection. + + """ if self.db_type.startswith('sqlite'): if not self.db_name: raise DbnameNoneException( @@ -78,11 +97,11 @@ class DBHelper(): self.db_type + self.db_name, encoding='utf-8', convert_unicode=True, connect_args={'check_same_thread': False}) else: - if all([self.user_name, self.passwrod, self.ip_address, self.port, self.db_name]): + if all([self.user_name, self.password, self.ip_address, self.port, self.db_name]): # create connection object self.engine = create_engine(URL(**{'database': self.db_name, 'username': self.user_name, - 'password': self.passwrod, + 'password': self.password, 'host': self.ip_address, 'port': self.port, 'drivername': self.db_type}), @@ -93,9 +112,14 @@ class DBHelper(): 'A disconnect is detected on a raw DB-API connection') def _db_file_path(self): - ''' - load the path stored in the sqlite database - ''' + """ + Description: load the path stored in the sqlite database + Args: + + Returns: + Raises: + + """ self.database_file_path = self._readconfig.get_system( 'data_base_path') if not self.database_file_path: @@ -104,9 +128,15 @@ class DBHelper(): os.makedirs(self.database_file_path) def __enter__(self): - ''' - functional description:Create a context manager for the database connection - ''' + """ + Description: functional description:Create a context manager for the database connection + Args: + + Returns: + Class instance + Raises: + + """ session = sessionmaker() if getattr(self, 'engine') is None: @@ -117,28 +147,42 @@ class DBHelper(): return self def __exit__(self, exc_type, exc_val, exc_tb): - ''' - functional description:Release the database connection pool and close the connection - ''' + """ + Description: functional description:Release the database connection pool and close the connection + Args: + + Returns: + exc_type: Abnormal type + exc_val: Abnormal value + exc_tb: Abnormal table + Raises: + """ self.session.close() @classmethod def create_all(cls, db_name=None): - ''' - functional description:Create all database tables - parameter: - return value: - exception description: - modify record: - ''' + """ + Description: functional description:Create all database tables + Args: + db_name: Database name + Returns: + + Raises: + + """ cls.BASE.metadata.create_all(bind=cls(db_name=db_name).engine) def create_table(self, tables): - ''' - Create a single table - ''' + """ + Description: Create a single table + Args: + tables: Table + Returns: + + Raises: + """ meta = MetaData(self.engine) for table_name in DBHelper.BASE.metadata.tables.keys(): if table_name in tables: @@ -147,12 +191,16 @@ class DBHelper(): table.create() def add(self, entity): - ''' - functional description:Insert a single data entity - parameter: - return value: + """ + Description: Insert a single data entity + Args: + entity: Data entity + Return: If the addition is successful, return the corresponding entity, otherwise return None - ''' + Raises: + ContentNoneException: An exception occurred while content is none + SQLAlchemyError: An exception occurred while creating the database + """ if entity is None: raise ContentNoneException( @@ -168,12 +216,17 @@ class DBHelper(): return entity def batch_add(self, dicts, model): - ''' - functional description:tables for adding databases in bulk - parameter: - :param dicts:Entity dictionary data to be added - :param model:Solid model class - ''' + """ + Description:tables for adding databases in bulk + Args: + dicts:Entity dictionary data to be added + model:Solid model class + Returns: + + Raises: + TypeError: An exception occurred while incoming type does not meet expectations + SQLAlchemyError: An exception occurred while creating the database + """ if model is None: raise ContentNoneException('solid model must be specified') diff --git a/packageship/packageship/pkgship.py b/packageship/packageship/pkgship.py index c22a5409..6fb34408 100644 --- a/packageship/packageship/pkgship.py +++ b/packageship/packageship/pkgship.py @@ -1,8 +1,12 @@ -''' - Entry method for custom commands -''' +#!/usr/bin/python3 +""" +Description: Entry method for custom commands +Class: BaseCommand,PkgshipCommand,RemoveCommand,InitDatabaseCommand,UpdateDatabaseCommand,AllPackageCommand, + UpdatePackageCommand,BuildDepCommand,InstallDepCommand,SelfBuildCommand,BeDependCommand,SingleCommand +""" import os import json + try: import argparse import requests @@ -13,6 +17,7 @@ try: from packageship.libs.log import Log from packageship.libs.exception import Error from packageship.libs.configutils.readconfig import ReadConfig + LOGGER = Log(__name__) except ImportError as import_error: print('Error importing related dependencies, \ @@ -21,14 +26,19 @@ else: from packageship.application.apps.package.function.constants import ResponseCode from packageship.application.apps.package.function.constants import ListNode - DB_NAME = 0 def main(): - ''' - command entry function - ''' + """ + Description: Command line tool entry, register related commands + Args: + + Returns: + + Raises: + Error: An error occurred while executing the command + """ try: packship_cmd = PkgshipCommand() packship_cmd.parser_args() @@ -38,11 +48,19 @@ def main(): class BaseCommand(): - ''' - Basic attributes used for command invocation - ''' + """ + Description: Basic attributes used for command invocation + Attributes: + write_host: Can write operation single host address + read_host: Can read the host address of the operation + headers: Send HTTP request header information + """ def __init__(self): + """ + Description: Class instance initialization + + """ self._read_config = ReadConfig() self.write_host = None self.read_host = None @@ -54,9 +72,14 @@ class BaseCommand(): self.load_write_host() def load_write_host(self): - ''' - Address to load write permission - ''' + """ + Description: Address to load write permission + Args: + + Returns: + Raises: + + """ wirte_port = self._read_config.get_system('write_port') write_ip = self._read_config.get_system('write_ip_addr') @@ -66,9 +89,14 @@ class BaseCommand(): setattr(self, 'write_host', _write_host) def load_read_host(self): - ''' - Address to load write permission - ''' + """ + Returns:Address to load read permission + Args: + + Returns: + Raises: + + """ read_port = self._read_config.get_system('query_port') read_ip = self._read_config.get_system('query_ip_addr') @@ -79,15 +107,23 @@ class BaseCommand(): class PkgshipCommand(BaseCommand): - ''' - PKG package command line - ''' + """ + Description: PKG package command line + Attributes: + statistics: Summarized data table + table: Output table + columns: Calculate the width of the terminal dynamically + params: Command parameters + """ parser = argparse.ArgumentParser( description='package related dependency management') subparsers = parser.add_subparsers( help='package related dependency management') def __init__(self): + """ + Description: Class instance initialization + """ super(PkgshipCommand, self).__init__() self.statistics = dict() self.table = PkgshipCommand.create_table( @@ -99,15 +135,28 @@ class PkgshipCommand(BaseCommand): @staticmethod def register_command(command): - ''' - Register command - ''' + """ + Description: Registration of related commands + + Args: + command: Related commands + + Returns: + Raises: + + """ command.register() def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ for command_params in self.params: self.parse.add_argument( # pylint: disable=E1101 command_params[0], @@ -117,9 +166,15 @@ class PkgshipCommand(BaseCommand): @classmethod def parser_args(cls): - ''' - Command parsing - ''' + """ + Description: Register the command line and parse related commands + Args: + + Returns: + + Raises: + Error: An error occurred during command parsing + """ cls.register_command(RemoveCommand()) cls.register_command(InitDatabaseCommand()) cls.register_command(UpdateDatabaseCommand()) @@ -137,9 +192,15 @@ class PkgshipCommand(BaseCommand): print('command error') def parse_package(self, response_data): - ''' - Parse the corresponding data of the package - ''' + """ + Description: Parse the corresponding data of the package + Args: + response_data: http request response content + Returns: + + Raises: + + """ if response_data.get('code') == ResponseCode.SUCCESS: package_all = response_data.get('data') if isinstance(package_all, list): @@ -151,9 +212,15 @@ class PkgshipCommand(BaseCommand): print(response_data.get('msg')) def parse_depend_package(self, response_data): - ''' - Parse the corresponding data of the package - ''' + """ + Description: Parsing package data with dependencies + Args: + response_data: http request response content + Returns: + Summarized data table + Raises: + + """ bin_package_count = 0 src_package_count = 0 if response_data.get('code') == ResponseCode.SUCCESS: @@ -179,7 +246,7 @@ class PkgshipCommand(BaseCommand): if bin_package not in \ self.statistics[package_depend[ListNode.DBNAME]]['binary']: self.statistics[package_depend[ListNode.DBNAME] - ]['binary'].append(bin_package) + ]['binary'].append(bin_package) bin_package_count += 1 # Determine whether the source package exists if package_depend[ListNode.SOURCE_NAME] not in \ @@ -198,9 +265,17 @@ class PkgshipCommand(BaseCommand): return statistics_table def print_(self, content=None, character='=', dividing_line=False): - ''' - Output formatted characters - ''' + """ + Description: Output formatted characters + Args: + content: Output content + character: Output separator content + dividing_line: Whether to show the separator + Returns: + + Raises: + + """ # Get the current width of the console if dividing_line: @@ -212,9 +287,15 @@ class PkgshipCommand(BaseCommand): @staticmethod def create_table(title): - ''' - Create printed forms - ''' + """ + Description: Create printed forms + Args: + title: Table title + Returns: + ASCII format table + Raises: + + """ table = PrettyTable(title) # table.set_style(prettytable.PLAIN_COLUMNS) table.align = 'l' @@ -225,9 +306,16 @@ class PkgshipCommand(BaseCommand): return table def statistics_table(self, bin_package_count, src_package_count): - ''' - Generate data for total statistical tables - ''' + """ + Description: Generate data for total statistical tables + Args: + bin_package_count: Number of binary packages + src_package_count: Number of source packages + Returns: + Summarized data table + Raises: + + """ statistics_table = self.create_table(['', 'binary', 'source']) statistics_table.add_row( ['self depend sum', bin_package_count, src_package_count]) @@ -240,9 +328,15 @@ class PkgshipCommand(BaseCommand): @staticmethod def http_error(response): - ''' - Log error messages for http - ''' + """ + Description: Log error messages for http + Args: + response: Response content of http request + Returns: + + Raises: + HTTPError: http request error + """ try: print(response.raise_for_status()) except HTTPError as http_error: @@ -252,27 +346,46 @@ class PkgshipCommand(BaseCommand): class RemoveCommand(PkgshipCommand): - ''' - Delete database command - ''' + """ + Description: Delete database command + Attributes: + parse: Command line parsing example + params: Command line parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(RemoveCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( 'rm', help='delete database operation') self.params = [('db', 'str', 'name of the database operated', '')] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(RemoveCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnErr: Request connection error + + """ if params.db is None: print('No database specified for deletion') else: @@ -296,11 +409,17 @@ class RemoveCommand(PkgshipCommand): class InitDatabaseCommand(PkgshipCommand): - ''' - Initialize database command - ''' + """ + Description: Initialize database command + Attributes: + parse: Command line parsing example + params: Command line parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(InitDatabaseCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( 'init', help='initialization of the database') @@ -308,22 +427,34 @@ class InitDatabaseCommand(PkgshipCommand): ('-filepath', 'str', 'name of the database operated', '')] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(InitDatabaseCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + + """ file_path = params.filepath try: response = requests.post(self.write_host + '/initsystem', data=json.dumps({'configfile': file_path}), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -339,11 +470,17 @@ class InitDatabaseCommand(PkgshipCommand): class UpdateDatabaseCommand(PkgshipCommand): - ''' - update database command - ''' + """ + Description: update database command + Attributes: + parse: Command line parsing example + params: Command line parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(UpdateDatabaseCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -351,25 +488,44 @@ class UpdateDatabaseCommand(PkgshipCommand): self.params = [('db', 'str', 'name of the database operated', '')] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(UpdateDatabaseCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + + Returns: + + Raises: + + """ pass # pylint: disable= W0107 class AllPackageCommand(PkgshipCommand): - ''' - get all package commands - ''' + """ + Description: get all package commands + Attributes: + parse: Command line parsing example + params: Command line parameters + table: Output table + """ def __init__(self): + """ + Description: Class instance initialization + """ super(AllPackageCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -379,18 +535,30 @@ class AllPackageCommand(PkgshipCommand): self.params = [('-db', 'str', 'name of the database operated', '')] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(AllPackageCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnectionError: Request connection error + """ _url = self.read_host + \ - '/packages?dbName={dbName}'.format(dbName=params.db) + '/packages?dbName={dbName}'.format(dbName=params.db) try: response = requests.get(_url) except ConnectionError as conn_error: @@ -407,11 +575,17 @@ class AllPackageCommand(PkgshipCommand): class UpdatePackageCommand(PkgshipCommand): - ''' - update package data - ''' + """ + Description: update package data + Attributes: + parse: Command line parsing example + params: Command line parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(UpdatePackageCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -424,16 +598,28 @@ class UpdatePackageCommand(PkgshipCommand): ] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(UpdatePackageCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnectionError: Request connection error + """ _url = self.write_host + '/packages/findByPackName' try: response = requests.put( @@ -458,11 +644,19 @@ class UpdatePackageCommand(PkgshipCommand): class BuildDepCommand(PkgshipCommand): - ''' - query the compilation dependencies of the specified package - ''' + """ + Description: query the compilation dependencies of the specified package + Attributes: + parse: Command line parsing example + params: Command line parameters + collection: Is there a collection parameter + collection_params: Command line collection parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(BuildDepCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -476,9 +670,15 @@ class BuildDepCommand(PkgshipCommand): ] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(BuildDepCommand, self).register() # collection parameters @@ -488,9 +688,15 @@ class BuildDepCommand(PkgshipCommand): self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnectionError: Request connection error + """ _url = self.read_host + '/packages/findBuildDepend' try: response = requests.post( @@ -515,11 +721,19 @@ class BuildDepCommand(PkgshipCommand): class InstallDepCommand(PkgshipCommand): - ''' - query the installation dependencies of the specified package - ''' + """ + Description: query the installation dependencies of the specified package + Attributes: + parse: Command line parsing example + params: Command line parameters + collection: Is there a collection parameter + collection_params: Command line collection parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(InstallDepCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -533,9 +747,15 @@ class InstallDepCommand(PkgshipCommand): ] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(InstallDepCommand, self).register() # collection parameters @@ -545,9 +765,15 @@ class InstallDepCommand(PkgshipCommand): self.parse.set_defaults(func=self.do_command) def parse_package(self, response_data): - ''' - Parse the corresponding data of the package - ''' + """ + Description: Parse the corresponding data of the package + Args: + response_data: http response data + Returns: + + Raises: + + """ if getattr(self, 'statistics'): setattr(self, 'statistics', dict()) bin_package_count = 0 @@ -573,7 +799,7 @@ class InstallDepCommand(PkgshipCommand): if bin_package not in \ self.statistics[package_depend[ListNode.DBNAME]]['binary']: self.statistics[package_depend[ListNode.DBNAME] - ]['binary'].append(bin_package) + ]['binary'].append(bin_package) bin_package_count += 1 # Determine whether the source package exists if package_depend[ListNode.SOURCE_NAME] not in \ @@ -593,9 +819,15 @@ class InstallDepCommand(PkgshipCommand): return statistics_table def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: Command line parameters + Returns: + + Raises: + ConnectionError: requests connection error + """ _url = self.read_host + '/packages/findInstallDepend' try: response = requests.post(_url, data=json.dumps( @@ -621,11 +853,19 @@ class InstallDepCommand(PkgshipCommand): class SelfBuildCommand(PkgshipCommand): - ''' - self-compiled dependency query - ''' + """ + Description: self-compiled dependency query + Attributes: + parse: Command line parsing example + params: Command line parameters + collection: Is there a collection parameter + collection_params: Command line collection parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(SelfBuildCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -647,9 +887,15 @@ class SelfBuildCommand(PkgshipCommand): ] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(SelfBuildCommand, self).register() # collection parameters @@ -659,9 +905,16 @@ class SelfBuildCommand(PkgshipCommand): self.parse.set_defaults(func=self.do_command) def _parse_bin_package(self, bin_packages): - ''' - Parsing binary result data - ''' + """ + Description: Parsing binary result data + Args: + bin_packages: Binary package data + + Returns: + + Raises: + + """ bin_package_count = 0 if bin_packages: for bin_package, package_depend in bin_packages.items(): @@ -681,19 +934,26 @@ class SelfBuildCommand(PkgshipCommand): if bin_package not in \ self.statistics[package_depend[ListNode.DBNAME]]['binary']: self.statistics[package_depend[ListNode.DBNAME] - ]['binary'].append(bin_package) + ]['binary'].append(bin_package) bin_package_count += 1 self.bin_package_table.add_row(row_data) return bin_package_count - def _parse_src_package(self, src_apckages): - ''' - Source package data analysis - ''' + def _parse_src_package(self, src_packages): + """ + Description: Source package data analysis + Args: + src_packages: Source package + + Returns: + Source package data + Raises: + + """ src_package_count = 0 - if src_apckages: - for src_package, package_depend in src_apckages.items(): + if src_packages: + for src_package, package_depend in src_packages.items(): # distinguish whether the current data is the data of the root node if isinstance(package_depend, list): @@ -708,7 +968,7 @@ class SelfBuildCommand(PkgshipCommand): # Determine whether the current binary package exists if src_package not in self.statistics[package_depend[DB_NAME]]['source']: self.statistics[package_depend[DB_NAME] - ]['source'].append(src_package) + ]['source'].append(src_package) src_package_count += 1 self.src_package_table.add_row(row_data) @@ -716,9 +976,15 @@ class SelfBuildCommand(PkgshipCommand): return src_package_count def parse_package(self, response_data): - ''' - Parse the corresponding data of the package - ''' + """ + Description: Parse the corresponding data of the package + Args: + response_data: http response data + Returns: + Summarized data table + Raises: + + """ if getattr(self, 'statistics'): setattr(self, 'statistics', dict()) bin_package_count = 0 @@ -744,9 +1010,15 @@ class SelfBuildCommand(PkgshipCommand): return statistics_table def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: commands lines params + Returns: + + Raises: + ConnectionError: requests connection error + """ _url = self.read_host + '/packages/findSelfDepend' try: response = requests.post(_url, @@ -778,11 +1050,17 @@ class SelfBuildCommand(PkgshipCommand): class BeDependCommand(PkgshipCommand): - ''' - dependent query - ''' + """ + Description: dependent query + Attributes: + parse: Command line parsing example + params: Command line parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(BeDependCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -794,16 +1072,28 @@ class BeDependCommand(PkgshipCommand): ] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(BeDependCommand, self).register() self.parse.set_defaults(func=self.do_command) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: command lines params + Returns: + + Raises: + ConnectionError: requests connection error + """ _url = self.read_host + '/packages/findBeDepend' try: response = requests.post(_url, data=json.dumps( @@ -813,7 +1103,7 @@ class BeDependCommand(PkgshipCommand): 'withsubpack': str(params.w) } ), headers=self.headers) - except ConnectionError as conn_error: + except ConnErr as conn_error: LOGGER.logger.error(conn_error) print(str(conn_error)) else: @@ -831,11 +1121,17 @@ class BeDependCommand(PkgshipCommand): class SingleCommand(PkgshipCommand): - ''' - query single package information - ''' + """ + Description: query single package information + Attributes: + parse: Command line parsing example + params: Command line parameters + """ def __init__(self): + """ + Description: Class instance initialization + """ super(SingleCommand, self).__init__() self.parse = PkgshipCommand.subparsers.add_parser( @@ -846,16 +1142,28 @@ class SingleCommand(PkgshipCommand): ] def register(self): - ''' - Command line parameter injection - ''' + """ + Description: Command line parameter injection + Args: + + Returns: + + Raises: + + """ super(SingleCommand, self).register() self.parse.set_defaults(func=self.do_command) def parse_package(self, response_data): - ''' - Parse the corresponding data of the package - ''' + """ + Description: Parse the corresponding data of the package + Args: + response_data: http response data + Returns: + + Raises: + + """ show_field_name = ('sourceName', 'dbname', 'version', 'license', 'maintainer', 'maintainlevel') print_contents = [] @@ -867,10 +1175,9 @@ class SingleCommand(PkgshipCommand): if value is None: value = '' if key in show_field_name: - line_content = '%-15s:%s' % (key, value) print_contents.append(line_content) - print_contents.append('='*self.columns) + print_contents.append('=' * self.columns) else: print(response_data.get('msg')) if print_contents: @@ -878,12 +1185,18 @@ class SingleCommand(PkgshipCommand): self.print_(content=content) def do_command(self, params): - ''' - Action to execute command - ''' + """ + Description: Action to execute command + Args: + params: command lines params + Returns: + + Raises: + ConnectionError: requests connection error + """ _url = self.read_host + \ - '/packages/findByPackName?dbName={db_name}&sourceName={packagename}' \ - .format(db_name=params.db, packagename=params.packagename) + '/packages/findByPackName?dbName={db_name}&sourceName={packagename}' \ + .format(db_name=params.db, packagename=params.packagename) try: response = requests.get(_url) except ConnectionError as conn_error: -- Gitee From 1f6b1f3f8b41305ef78d57d05e7da45bca60fb42 Mon Sep 17 00:00:00 2001 From: gongzt Date: Wed, 8 Jul 2020 10:37:26 +0800 Subject: [PATCH 63/72] Misspelled password word in sqlhelper --- .../packageship/libs/dbutils/sqlalchemy_helper.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index b94c16ed..0650e168 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -25,16 +25,16 @@ class DBHelper(): # The base class inherited by the data model BASE = declarative_base() - def __init__(self, user_name=None, passwrod=None, ip_address=None, # pylint: disable=R0913 + def __init__(self, user_name=None, password=None, ip_address=None, # pylint: disable=R0913 port=None, db_name=None, db_type=None, **kwargs): self.user_name = user_name self._readconfig = ReadConfig() if self.user_name is None: self.user_name = self._readconfig.get_database('user_name') - self.passwrod = passwrod - if self.passwrod is None: - self.passwrod = self._readconfig.get_database('password') + self.password = password + if self.password is None: + self.password = self._readconfig.get_database('password') self.ip_address = ip_address @@ -79,11 +79,11 @@ class DBHelper(): self.db_type + self.db_name, encoding='utf-8', convert_unicode=True, connect_args={'check_same_thread': False}) else: - if all([self.user_name, self.passwrod, self.ip_address, self.port, self.db_name]): + if all([self.user_name, self.password, self.ip_address, self.port, self.db_name]): # create connection object self.engine = create_engine(URL(**{'database': self.db_name, 'username': self.user_name, - 'password': self.passwrod, + 'password': self.password, 'host': self.ip_address, 'port': self.port, 'drivername': self.db_type}), -- Gitee From 260738b889884db1a21b64410a6d9e458c21fc59 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Wed, 8 Jul 2020 10:40:20 +0800 Subject: [PATCH 64/72] Modify the file according to the comments specification --- .../apps/package/function/be_depend.py | 94 +++-- .../apps/package/function/build_depend.py | 53 ++- .../apps/package/function/install_depend.py | 92 +++-- .../apps/package/function/searchdb.py | 77 +++-- .../apps/package/function/self_depend.py | 122 +++++-- .../application/apps/package/view.py | 320 +++++++++++------- .../libs/configutils/readconfig.py | 52 ++- packageship/packageship/libs/exception/ext.py | 44 ++- 8 files changed, 557 insertions(+), 297 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/be_depend.py b/packageship/packageship/application/apps/package/function/be_depend.py index a6bf3cf8..abc1c150 100644 --- a/packageship/packageship/application/apps/package/function/be_depend.py +++ b/packageship/packageship/application/apps/package/function/be_depend.py @@ -1,9 +1,10 @@ -# -*- coding:utf-8 -*- -''' - The dependencies of the query package +#!/usr/bin/python3 +""" +Description:The dependencies of the query package Used for package deletion and upgrade scenarios This includes both install and build dependencies -''' +Class: BeDepend +""" from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql import literal_column @@ -12,33 +13,50 @@ from packageship.libs.dbutils import DBHelper from packageship.application.models.package import src_pack from packageship.application.apps.package.function.constants import ResponseCode + class BeDepend: - ''' - Find the dependencies of the source package - ''' + """ + Description: Find the dependencies of the source package + Attributes: + source_name: source name + db_name: database name + with_sub_pack: with_sub_pack + source_name_set:Source package lookup set + bin_name_set:Bin package lookup set + result_dict:return json + """ def __init__(self, source_name, db_name, with_sub_pack): - ''' - :param source_name: source_name - :param db_name: db_name - :param with_sub_pack: with_sub_pack - ''' + """ + init class + """ self.source_name = source_name self.db_name = db_name self.with_sub_pack = with_sub_pack - # Source package lookup set self.source_name_set = set() - # Bin package lookup set self.bin_name_set = set() - # return json self.result_dict = dict() def main(self): - ''' - Map the database, if the source package of the query is not in the database, + """ + Description: Map the database, if the source + package of the query is not in the database, throw an exception. Directly to the end - :return: - ''' + Args: + Returns: + "source name": [ + "source", + "version", + "dbname", + [ + [ + "root", + null + ] + ] + ] + Raises: + """ with DBHelper(db_name=self.db_name) as data_base: src_obj = data_base.session.query( src_pack).filter_by(name=self.source_name).first() @@ -57,13 +75,16 @@ class BeDepend: return self.result_dict def package_bedepend(self, pkg_id_list, data_base, package_type): - ''' - Query the dependent function - :param pkg_id_list:source or binary packages id - :param data_base: database - :param package_type: package type - :return: - ''' + """ + Description: Query the dependent function + Args: + pkg_id_list:source or binary packages id + data_base: database + package_type: package type + Returns: + Raises: + SQLAlchemyError: Database connection exception + """ search_set = set(pkg_id_list) id_in = literal_column('id').in_(search_set) # package_type @@ -171,14 +192,17 @@ class BeDepend: self.package_bedepend(bin_id_list, data_base, package_type="bin") def make_dicts(self, key, source_name, version, parent_node, be_type): - ''' - :param key: dependent bin name - :param source_name: source name - :param version: version - :param parent_node: Rely on package name - :param be_type: dependent type - :return: - ''' + """ + Description: Splicing dictionary function + Args: + key: dependent bin name + source_name: source name + version: version + parent_node: Rely on package name + be_type: dependent type + Returns: + Raises: + """ if key not in self.result_dict: self.result_dict[key] = [ source_name, @@ -191,7 +215,7 @@ class BeDepend: ] ] else: - if [parent_node,be_type] not in self.result_dict[key][-1]: + if [parent_node, be_type] not in self.result_dict[key][-1]: self.result_dict[key][-1].append([ parent_node, be_type diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py index e3bdac13..b69c80d6 100644 --- a/packageship/packageship/application/apps/package/function/build_depend.py +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -1,5 +1,7 @@ +#!/usr/bin/python3 """ -Find compilation dependency of source package +Description: Find compilation dependency of source package +class: BuildDepend """ from packageship.application.apps.package.function.searchdb import SearchDB from packageship.application.apps.package.function.install_depend import InstallDepend @@ -8,10 +10,21 @@ from packageship.application.apps.package.function.constants import ResponseCode class BuildDepend: """ - Find compilation dependency of source package + Description: Find compilation dependency of source package + Attributes: + pkg_name_list: List of package names + db_list: List of database names + self_build: Compile dependency conditions + history_dict: Query history dict + search_db:Query an instance of a database class + result_dict:A dictionary to store the data that needs to be echoed + source_dict:A dictionary to store the searched source code package name """ def __init__(self, pkg_name_list, db_list, self_build=0, history_dict=None): + """ + init class + """ self.pkg_name_list = pkg_name_list self._self_build = self_build @@ -25,8 +38,13 @@ class BuildDepend: def build_depend_main(self): """ - Entry function - :return: ResponseCode,result_dict,source_dict + Description: Entry function + Args: + Returns: + ResponseCode: response code + result_dict: Dictionary of query results + source_dict: Dictionary of source code package + Raises: """ if not self.search_db.db_object_dict: return ResponseCode.DIS_CONNECTION_DB, None, None @@ -52,8 +70,12 @@ class BuildDepend: def build_depend(self, pkg_list): """ - @:param pkg_list:You need to find the dependent source package name - :return ResponseCode + Description: Compile dependency query + Args: + pkg_list:You need to find the dependent source package name + Returns: + ResponseCode: response code + Raises: """ res_status, build_list = self.search_db.get_build_depend(pkg_list) @@ -84,11 +106,14 @@ class BuildDepend: def _create_node_and_get_search_list(self, build_list, pkg_list): """ - To create root node in self.result_dict and + Description: To create root node in self.result_dict and return the name of the source package to be found next time - @:param build_list:List of binary package names - @:param pkg_list: List of binary package names - :return the name of the source package to be found next time + Args: + build_list:List of binary package names + pkg_list: List of binary package names + Returns: + the name of the source package to be found next time + Raises: """ search_set = set() search_list = [] @@ -150,9 +175,11 @@ class BuildDepend: def self_build(self, pkg_name_li): """ - Using recursion to find compilation dependencies - :param pkg_name_li: Source package name list - :return: + Description: Using recursion to find compilation dependencies + Args: + pkg_name_li: Source package name list + Returns: + Raises: """ if not pkg_name_li: return diff --git a/packageship/packageship/application/apps/package/function/install_depend.py b/packageship/packageship/application/apps/package/function/install_depend.py index 6d9bf57f..8e5c1e7e 100644 --- a/packageship/packageship/application/apps/package/function/install_depend.py +++ b/packageship/packageship/application/apps/package/function/install_depend.py @@ -1,7 +1,9 @@ -''' - Querying for install dependencies - Querying packages install depend for those package can be installed -''' +#!/usr/bin/python3 +""" +Description: Querying for install dependencies + Querying packages install depend for those package can be installed +class: InstallDepend, DictionaryOperations +""" from packageship.libs.log import Log from .searchdb import SearchDB from .constants import ResponseCode @@ -11,15 +13,20 @@ LOGGER = Log(__name__) class InstallDepend(): - ''' - Description: quert install depend of package + """ + Description: query install depend of package + Attributes: + db_list: A list of Database name to show the priority + __search_list: Contain the binary packages searched in the next loop + binary_dict: Contain all the binary packages info and operation + __search_db: A object of database which would be connected changeLog: - ''' + """ #pylint: disable = too-few-public-methods def __init__(self, db_list): - ''' - :param db_list: A list of Database name to show the priority - ''' + """ + Initialization class + """ self.binary_dict = DictionaryOperations() self.__search_list = [] @@ -27,12 +34,14 @@ class InstallDepend(): self.__search_db = SearchDB(db_list) def query_install_depend(self, binary_list, history_dicts=None): - ''' + """ Description: init result dict and determint the loop end point - :param binary_list: A list of binary rpm package name - history_dicts: record the searching install depend history, + Args: + binary_list: A list of binary rpm package name + history_dicts: record the searching install depend history, defualt is None - :return binary_dict.dictionary: + Returns: + binary_dict.dictionary: {binary_name: [ src, dbname, @@ -42,7 +51,8 @@ class InstallDepend(): 'install' ] ]} - ''' + Raises: + """ if not self.__search_db.db_object_dict: return ResponseCode.DIS_CONNECTION_DB, None if not binary_list: @@ -58,10 +68,12 @@ class InstallDepend(): def __query_single_install_dep(self, history_dicts): """ - Description: query a package install depend and append to result - :param history_dicts - :return response_code - changeLog: + Description: query a package install depend and append to result + Args: + history_dicts: A list of binary rpm package name + Returns: + response_code: response code + Raises: """ result_list = self.__search_db.get_install_depend(self.__search_list) for search in self.__search_list: @@ -98,28 +110,50 @@ class InstallDepend(): parent_node=[[result.search_name, 'install']]) self.__search_list.append(result.depend_name) + class DictionaryOperations(): - ''' - Related to dictionary operations, creating dictionary, append dictionary - ''' + """ + Description: Related to dictionary operations, creating dictionary, append dictionary + Attributes: + dictionary: Contain all the binary packages info after searching + changeLog: + """ def __init__(self): - + """ + init class + """ self.dictionary = dict() def init_key(self, key, src=None, version=None, dbname=None, parent_node=None): - ''' - Creating dictionary - ''' + """ + Description: Creating dictionary + Args: + key: binary_name + src: source_name + version: version + dbname: databases name + parent_node: parent_node + Returns: + dictionary[key]: [src, version, dbname, parent_node] + """ if dbname: self.dictionary[key] = [src, version, dbname, [['root', None]]] else: self.dictionary[key] = [src, version, dbname, parent_node] def update_value(self, key, src=None, version=None, dbname=None, parent_node=None): - ''' - append dictionary - ''' + """ + Description: append dictionary + Args: + key: binary_name + src: source_name + version: version + dbname: database name + parent_node: parent_node + Returns: + Raises: + """ if src: self.dictionary[key][ListNode.SOURCE_NAME] = src if version: diff --git a/packageship/packageship/application/apps/package/function/searchdb.py b/packageship/packageship/application/apps/package/function/searchdb.py index a3a1d680..bba2994b 100644 --- a/packageship/packageship/application/apps/package/function/searchdb.py +++ b/packageship/packageship/application/apps/package/function/searchdb.py @@ -1,5 +1,8 @@ +#!/usr/bin/python3 """ - A set for all query databases function +Description: A set for all query databases function +class: SearchDB +functions: db_priority """ from collections import namedtuple @@ -22,6 +25,9 @@ LOGGER = Log(__name__) class SearchDB(): """ Description: query in database + Attributes: + db_list: Database list + db_object_dict:A dictionary for storing database connection objects changeLog: """ def __new__(cls, *args, **kwargs): @@ -31,6 +37,9 @@ class SearchDB(): return cls._instance def __init__(self, db_list): + """ + init class + """ self.db_object_dict = dict() for db_name in db_list: try: @@ -44,9 +53,11 @@ class SearchDB(): Description: get a package install depend from database: binary_name -> binary_id -> requires_set -> requires_id_set -> provides_set -> install_depend_binary_id_key_list -> install_depend_binary_name_list - :param binary_lsit: a list of binary package name - :return install depend list - changeLog: + Args: + binary_list: a list of binary package name + Returns: + install depend list + Raises: """ result_list = [] get_list = [] @@ -111,9 +122,15 @@ class SearchDB(): """ Description: get a package source name from database: bianry_name ->binary_source_name -> source_name - input: search package's name, database preority list - return: database name, source name - changeLog: + Args: + binary_name: search package's name, database preority list + Returns: + db_name: database name + source_name: source name + source_version: source version + Raises: + AttributeError: The object does not have this property + SQLAlchemyError: sqlalchemy error """ for db_name, data_base in self.db_object_dict.items(): try: @@ -136,9 +153,13 @@ class SearchDB(): """ Description: get a subpack list based on source name list: source_name ->source_name_id -> binary_name - input: search package's name, database preority list - return: subpack tuple - changeLog: + Args: + source_name_list: search package's name, database preority list + Returns: + result_list: subpack tuple + Raises: + AttributeError: The object does not have this property + SQLAlchemyError: sqlalchemy error """ if not self.db_object_dict: return ResponseCode.DIS_CONNECTION_DB, None @@ -190,14 +211,19 @@ class SearchDB(): def get_binary_in_other_database(self, not_found_binary, db_): """ - Binary package name data not found in the current database, go to other databases to try - @:param:not_found_build These data cannot be found in the current database - @:param:db:current database name - return:a list :[(search_name,source_name,bin_name, + Description: Binary package name data not found in + the current database, go to other databases to try + Args: + not_found_binary: not_found_build These data cannot be found in the current database + db_:current database name + Returns: + a list :[(search_name,source_name,bin_name, bin_version,db_name,search_version,req_name), (search_name,source_name,bin_name, bin_version,db_name,search_version,req_name),] - changeLog:new method to look for data in other databases + Raises: + AttributeError: The object does not have this property + SQLAlchemyError: sqlalchemy error """ if not not_found_binary: return [] @@ -264,13 +290,16 @@ class SearchDB(): def get_build_depend(self, source_name_li): """ Description: get a package build depend from database - input: - @:param: search package's name list - return: all source pkg build depend list - structure :[(search_name,source_name,bin_name,bin_version,db_name,search_version), + Args: + source_name_li: search package's name list + Returns: + all source pkg build depend list + structure :[(search_name,source_name,bin_name,bin_version,db_name,search_version), (search_name,source_name,bin_name,bin_version,db_name,search_version),] - changeLog: Modify SQL logic and modify return content by:zhangtao + Raises: + AttributeError: The object does not have this property + SQLAlchemyError: sqlalchemy error """ # pylint: disable=R0914 return_tuple = namedtuple("return_tuple", [ @@ -372,7 +401,13 @@ class SearchDB(): def db_priority(): """ - return dbprioty + Description: Read yaml file, return database name, according to priority + Args: + Returns: + db_list: database name list + Raises: + FileNotFoundError: file cannot be found + Error: abnormal error """ try: with open(DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: diff --git a/packageship/packageship/application/apps/package/function/self_depend.py b/packageship/packageship/application/apps/package/function/self_depend.py index d2bfc677..e63d97c7 100644 --- a/packageship/packageship/application/apps/package/function/self_depend.py +++ b/packageship/packageship/application/apps/package/function/self_depend.py @@ -1,8 +1,10 @@ -''' - Querying for self dependencies +#!/usr/bin/python3 +""" +Description: Querying for self dependencies Querying packages install and build depend for those package can be build and install -''' +class: SelfDepend, DictionaryOperations +""" import copy from packageship.libs.log import Log @@ -14,13 +16,28 @@ from .build_depend import BuildDepend as build_depend LOGGER = Log(__name__) + class SelfDepend(): - ''' + """ + Description: Querying for self dependencies Querying packages install and build depend for those package can be build and install - ''' + Attributes: + db_list: list of database names + binary_dict: Contain all the binary packages info and operation + source_dicts: Contain all the source packages info and operation + result_tmp: restore the return result dict + search_install_list: Contain the binary packages searched install dep in the next loop + search_build_list: Contain the source packages searched build dep in the next loop + search_subpack_list: Contain the source packages searched subpack in the next loop + withsubpack: withsubpack + search_db: A object of database which would be connected + """ def __init__(self, db_list): + """ + init class + """ self.binary_dict = DictionaryOperations() self.source_dicts = DictionaryOperations() self.result_tmp = dict() @@ -34,9 +51,15 @@ class SelfDepend(): def query_depend(self, packname, selfbuild, withsubpack, packtype='binary'): """ Description: init result dict and determint the loop end point - input: packname, selfbuild, withsubpack, packtype - return: result dicts, source dicts - changeLog: + Args: + packname: Package name + selfbuild: selfbuild + withsubpack: withsubpack + packtype: package type + Returns: + binary_dict.dictionary: Contain all the binary packages info after searching + source_dicts.dictionary: Contain all the source packages info after searching + Raises: """ if not self.search_db.db_object_dict: return ResponseCode.DIS_CONNECTION_DB, None, None @@ -67,9 +90,12 @@ class SelfDepend(): def init_dict(self, packname, packtype): """ Description: init result dict - input: packname, packtype - return: response_code - changeLog: + Args: + packname: package name + packtype: package type + Returns: + response_code + Raises: """ if packtype == 'source': response_code, subpack_list = self.search_db.get_sub_pack([packname]) @@ -99,8 +125,9 @@ class SelfDepend(): def query_install(self): """ Description: query install depend - return: - changeLog: + Args: + Returns: + Raises: """ self.result_tmp.clear() _, self.result_tmp = \ @@ -131,8 +158,9 @@ class SelfDepend(): def with_subpack(self): """ Description: query subpackage - return: - changeLog: + Args: + Returns: + Raises: """ if None in self.search_subpack_list: LOGGER.logger.warning("There is a NONE in input value:" + \ @@ -153,8 +181,10 @@ class SelfDepend(): def query_build(self, selfbuild): """ Description: query build depend - return: - changeLog: + Args: + selfbuild: selfbuild + Returns: + Raises: """ self.result_tmp.clear() if selfbuild == 0: @@ -165,8 +195,9 @@ class SelfDepend(): def query_builddep(self): """ Description: for selfbuild == 0, query selfbuild depend - return: - changeLog: + Args: + Returns: + Raises: """ _, self.result_tmp, _ = build_depend( self.search_build_list, @@ -197,8 +228,8 @@ class SelfDepend(): def query_selfbuild(self): """ Description: for selfbuild == 1, query selfbuild depend - return: - changeLog: + Args: + Returns: """ _, self.result_tmp, source_dicts_tmp = build_depend( self.search_build_list, @@ -228,32 +259,55 @@ class SelfDepend(): class DictionaryOperations(): - ''' - Related to dictionary operations, creating dictionary, append dictionary - ''' + """ + Description: Related to dictionary operations, creating dictionary, append dictionary + Attributes: + dictionary: dict + """ def __init__(self): - + """ + init class + """ self.dictionary = dict() def append_src(self, key, dbname, version): - ''' - Appending source dictionary - ''' + """ + Description: Appending source dictionary + Args: + key: bianry name + dbname: database name + version: version + Returns: + Raises: + """ self.dictionary[key] = [dbname, version] def append_bin(self, key, src=None, version=None, dbname=None, parent_node=None): - ''' - Appending binary dictionary - ''' + """ + Description: Appending binary dictionary + Args: + key: binary name + src: source name + version: version + dbname: database name + parent_node: parent node + Returns: + Raises: + """ if not parent_node: self.dictionary[key] = [src, version, dbname, [['root', None]]] else: self.dictionary[key] = [src, version, dbname, [parent_node]] def update_value(self, key, parent_list=None): - ''' - append dictionary - ''' + """ + Args: + key: binary name + parent_list: parent list + Returns: + Raises: + """ + if parent_list: self.dictionary[key][ListNode.PARENT_LIST].extend(parent_list) diff --git a/packageship/packageship/application/apps/package/view.py b/packageship/packageship/application/apps/package/view.py index 7a560af7..554ec137 100644 --- a/packageship/packageship/application/apps/package/view.py +++ b/packageship/packageship/application/apps/package/view.py @@ -1,5 +1,8 @@ +#!/usr/bin/python3 """ -view: Request logic processing Return json format +description: Interface processing +class: BeDepend, BuildDepend, InitSystem, InstallDepend, Packages, +Repodatas, SelfDepend, SinglePack """ import yaml from flask import request @@ -41,22 +44,38 @@ LOGGER = Log(__name__) class Packages(Resource): - ''' + """ Description: interface for package info management Restful API: get changeLog: - ''' + """ def get(self): - ''' + """ Description: Get all package info from a database - input: - dbName - return: - json file contain package's info - Exception: - Changelog: - ''' + Args: + dbName: Data path name, not required parameter + Returns: + { + "code": "", + "data": [ + { + "dbname": "", + "downloadURL": "", + "license": "", + "maintainer": , + "maintainlevel": , + "sourceName": "", + "sourceURL": "", + "version": "" + }, + "msg": "" + } + Raises: + DisconnectionError: Unable to connect to database exception + AttributeError: Object does not have this property + Error: Abnormal error + """ # Get verification parameters schema = PackagesSchema() data = schema.dump(request.args) @@ -96,24 +115,45 @@ class Packages(Resource): ResponseCode.response_json( ResponseCode.DIS_CONNECTION_DB)) + class SinglePack(Resource): - ''' + """ description: single package management - Restful API: get, put + Restful API: get、put ChangeLog: - ''' + """ def get(self): - ''' + """ description: Searching a package info - input: - sourceName - dbName - return: - json file contain package's detailed info - exception: - changeLog: - ''' + Args: + dbName: Database name, not required parameter + sourceName: Source code package name, must pass + Returns: + { + "code": "", + "data": [ + { + "buildDep": [], + "dbname": "", + "downloadURL": "", + "license": "", + "maintainer": "", + "maintainlevel": "", + "sourceName": "", + "sourceURL": "", + "subpack": {}, + "version": "" + } + ], + "msg": "" + } + Raises: + DisconnectionError: Unable to connect to database exception + AttributeError: Object does not have this property + TypeError: Exception of type + Error: Abnormal error + """ # Get verification parameters schema = GetpackSchema() data = schema.dump(request.args) @@ -160,17 +200,25 @@ class SinglePack(Resource): return jsonify(ResponseCode.response_json(abnormal)) def put(self): - ''' - Description: update a package info - input: - packageName - dbName - maintainer - maintainLevel - return: - exception: - changeLog: - ''' + """ + Description: update a package info, + Args: + dbName: Database name,Parameters are required + sourceName: The name of the source code package. Parameters are required + maintainer: Maintainer, parameter not required + maintainlevel: Maintenance level, parameter not required + Returns: + { + "code": "", + "data": "", + "msg": "" + } + Raises: + DisconnectionError: Unable to connect to database exception + AttributeError: Object does not have this property + TypeError: Exception of type + Error: Abnormal error + """ # Get verification parameters schema = PutpackSchema() data = schema.dump(request.get_json()) @@ -215,38 +263,27 @@ class SinglePack(Resource): class InstallDepend(Resource): - ''' + """ Description: install depend of binary package Restful API: post changeLog: - ''' + """ def post(self): - ''' + """ Description: Query a package's install depend(support querying in one or more databases) - input: - binaryName - dbPreority: the array for database preority - return: - resultDict{ - binary_name: //binary package name - [ - src, //the source package name for - that binary packge - dbname, - version, - [ - parent_node, //the binary package name which is - the install depend for binaryName - type //install install or build, which - depend on the function - ] - ] + Args: + binaryName:name of the bin package + dbPreority:the array for database preority + Returns: + { + "code": "", + "data": "", + "msg": "" } - exception: - changeLog: - ''' + Raises: + """ schema = InstallDependSchema() data = request.get_json() @@ -292,35 +329,28 @@ class InstallDepend(Resource): class BuildDepend(Resource): - ''' + """ Description: build depend of binary package Restful API: post changeLog: - ''' + """ def post(self): - ''' + """ Description: Query a package's build depend and build depend package's install depend (support querying in one or more databases) - input: - sourceName: - dbPreority: the array for database preority - return: - resultList[ - restult[ - binaryName: - srcName: - dbName: - type: install or build, which depend - on the function - parentNode: the binary package name which is - the build/install depend for binaryName - ] - ] - exception: - changeLog: - ''' + Args: + sourceName :name of the source package + dbPreority:the array for database preority + Returns: + { + "code": "", + "data": "", + "msg": "" + } + Raises: + """ schema = BuildDependSchema() data = request.get_json() @@ -360,38 +390,31 @@ class BuildDepend(Resource): class SelfDepend(Resource): - ''' + """ Description: querying install and build depend for a package and others which has the same src name Restful API: post changeLog: - ''' + """ def post(self): - ''' - description: Query a package's all dependencies including install and build depend + """ + Description: Query a package's all dependencies including install and build depend (support quering a binary or source package in one or more databases) - input: - packageName: + Args: + packageName:package name packageType: source/binary selfBuild :0/1 withSubpack: 0/1 - dbPreority: the array for database preority - return: - resultList[ - restult[ - binaryName: - srcName: - dbName: - type: install or build, which depend on the function - parentNode: the binary package name which is the - build/install depend for binaryName - ] - ] - - exception: - changeLog: - ''' + dbPreority:the array for database preority + Returns: + { + "code": "", + "data": "", + "msg": "" + } + Raises: + """ schema = SelfDependSchema() data = request.get_json() @@ -440,22 +463,22 @@ class SelfDepend(Resource): class BeDepend(Resource): - ''' + """ Description: querying be installed and built depend for a package and others which has the same src name Restful API: post changeLog: - ''' + """ def post(self): - ''' + """ description: Query a package's all dependencies including be installed and built depend - input: - packageName: + Args: + packageName:package name withSubpack: 0/1 - dbname: - return: + dbname:database name + Returns: resultList[ restult[ binaryName: @@ -468,7 +491,7 @@ class BeDepend(Resource): ] exception: changeLog: - ''' + """ schema = BeDependSchema() data = request.get_json() validate_err = schema.validate(data) @@ -501,27 +524,41 @@ class BeDepend(Resource): class Repodatas(Resource): - """API for operating databases""" + """ + description: Get database information and delete database + Restful API: get, delete + ChangeLog: + """ def get(self): - ''' + """ description: get all database - input: - return: - databasesName - status - priority - exception: - changeLog: - ''' + Args: + Returns: + { + "code": "", + "data": [ + { + "database_name": "", + "priority": "", + "status": "" + } + ], + "msg": "" + } + Raises: + FileNotFoundError: File not found exception + TypeError: Exception of wrong type + Error: abnormal Error + """ try: with open(DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: init_database_date = yaml.load( file_context.read(), Loader=yaml.FullLoader) if init_database_date is None: raise ContentNoneException( - "The content of the database initialization configuration " - "file cannot be empty ") + "The content of the database initialization configuration " + "file cannot be empty") init_database_date.sort( key=lambda x: x['priority'], reverse=False) return jsonify( @@ -535,11 +572,20 @@ class Repodatas(Resource): ) def delete(self): - ''' + """ description: get all database - input: database name - return: success or failure - ''' + Args: + Returns: + { + "code": "", + "data": "", + "msg": "" + } + Raises: + FileNotFoundError: File not found exception, + TypeError: Exception of wrong type + Error: Abnormal error + """ schema = DeletedbSchema() data = schema.dump(request.args) if schema.validate(data): @@ -570,15 +616,29 @@ class Repodatas(Resource): class InitSystem(Resource): - '''InitSystem''' + """ + description: Initialize database + Restful API: post + ChangeLog: + """ def post(self): """ description: InitSystem - input: - return: - exception: - changeLog: + Args: + Returns: + { + "code": "", + "data": "", + "msg": "" + } + Raises: + ContentNoneException: Unable to connect to the exception of the database + DisconnectionError:Exception connecting to database + TypeError:Exception of wrong type + DataMergeException:Exception of merging data + FileNotFoundError:File not found exception + Error: abnormal Error """ schema = InitSystemSchema() diff --git a/packageship/packageship/libs/configutils/readconfig.py b/packageship/packageship/libs/configutils/readconfig.py index 8e2aeeae..a94f504d 100644 --- a/packageship/packageship/libs/configutils/readconfig.py +++ b/packageship/packageship/libs/configutils/readconfig.py @@ -1,8 +1,10 @@ -''' - Read the base class of the configuration file in the system - which mainly includes obtaining specific node values - and obtaining arbitrary node values -''' +#!/usr/bin/python3 +""" + Description:Read the base class of the configuration file in the system + which mainly includes obtaining specific node values + and obtaining arbitrary node values + Class:ReadConfig +""" import configparser from configparser import NoSectionError from configparser import NoOptionError @@ -10,18 +12,25 @@ from packageship.system_config import SYS_CONFIG_PATH class ReadConfig: - ''' - Read the configuration file base class in the system - ''' + """ + Description: Read the configuration file base class in the system + Attributes: + conf:Configuration file for the system + conf.read:Read the system configuration file + """ def __init__(self): self.conf = configparser.ConfigParser() self.conf.read(SYS_CONFIG_PATH) def get_system(self, param): - ''' - Get any data value under the system configuration node - ''' + """ + Description: Get any data value under the system configuration node + Args: + param:The node parameters that need to be obtained + Returns: + Raises: + """ if param: try: return self.conf.get("SYSTEM", param) @@ -32,9 +41,13 @@ class ReadConfig: return None def get_database(self, param): - ''' - Get any data value under the database configuration node - ''' + """ + Description: Get any data value under the database configuration node + Args: + param:The node parameters that need to be obtained + Returns: + Raises: + """ if param: try: return self.conf.get("DATABASE", param) @@ -45,9 +58,14 @@ class ReadConfig: return None def get_config(self, node, param): - ''' - Get configuration data under any node - ''' + """ + Description: Get configuration data under any node + Args: + node:node + param:The node parameters that need to be obtained + Returns: + Raises: + """ if all([node, param]): try: return self.conf.get(node, param) diff --git a/packageship/packageship/libs/exception/ext.py b/packageship/packageship/libs/exception/ext.py index 12a96ef6..697d8e21 100644 --- a/packageship/packageship/libs/exception/ext.py +++ b/packageship/packageship/libs/exception/ext.py @@ -1,13 +1,17 @@ -''' - System exception information -''' - +#!/usr/bin/python3 +""" +Description:System exception information +Class:Error,ContentNoneException,DbnameNoneException, + DatabaseRepeatException,DataMergeException +""" class Error(Exception): + """ - Base class for ConfigParser exceptions + Description: Read the configuration file base class in the system + Attributes: + message:Exception information """ - def __init__(self, msg=''): self.message = msg Exception.__init__(self, msg) @@ -19,36 +23,40 @@ class Error(Exception): class ContentNoneException(Error): - ''' - Content is empty exception - ''' + """ + Description: Content is empty exception + Attributes: + """ def __init__(self, message): Error.__init__(self, 'No content: %r' % (message,)) class DbnameNoneException(ContentNoneException): - ''' - Exception with empty database name - ''' + """ + Description: Exception with empty database name + Attributes: + """ def __init__(self, message): ContentNoneException.__init__(self, '%r' % (message,)) class DatabaseRepeatException(Error): - ''' - There are duplicate exceptions in the database - ''' + """ + Description: There are duplicate exceptions in the database + Attributes: + """ def __init__(self, message): Error.__init__(self, 'Database repeat: %r' % (message,)) class DataMergeException(Error): - ''' - abnormal integration data - ''' + """ + Description: abnormal integration data + Attributes: + """ def __init__(self, message): Error.__init__(self, 'DataMerge exception: %r' % (message,)) -- Gitee From 7e9c9e54692ae159321b325c0320352fa0a19b73 Mon Sep 17 00:00:00 2001 From: bcq <814063667@qq.com> Date: Wed, 8 Jul 2020 10:44:35 +0800 Subject: [PATCH 65/72] Modify the file according to the comments specification --- .../packageship/application/apps/package/function/be_depend.py | 2 +- .../application/apps/package/function/build_depend.py | 2 +- packageship/packageship/libs/configutils/readconfig.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packageship/packageship/application/apps/package/function/be_depend.py b/packageship/packageship/application/apps/package/function/be_depend.py index abc1c150..84654af7 100644 --- a/packageship/packageship/application/apps/package/function/be_depend.py +++ b/packageship/packageship/application/apps/package/function/be_depend.py @@ -14,7 +14,7 @@ from packageship.application.models.package import src_pack from packageship.application.apps.package.function.constants import ResponseCode -class BeDepend: +class BeDepend(): """ Description: Find the dependencies of the source package Attributes: diff --git a/packageship/packageship/application/apps/package/function/build_depend.py b/packageship/packageship/application/apps/package/function/build_depend.py index b69c80d6..672cbe60 100644 --- a/packageship/packageship/application/apps/package/function/build_depend.py +++ b/packageship/packageship/application/apps/package/function/build_depend.py @@ -8,7 +8,7 @@ from packageship.application.apps.package.function.install_depend import Install from packageship.application.apps.package.function.constants import ResponseCode, ListNode -class BuildDepend: +class BuildDepend(): """ Description: Find compilation dependency of source package Attributes: diff --git a/packageship/packageship/libs/configutils/readconfig.py b/packageship/packageship/libs/configutils/readconfig.py index a94f504d..3c9cc2bc 100644 --- a/packageship/packageship/libs/configutils/readconfig.py +++ b/packageship/packageship/libs/configutils/readconfig.py @@ -11,7 +11,7 @@ from configparser import NoOptionError from packageship.system_config import SYS_CONFIG_PATH -class ReadConfig: +class ReadConfig(): """ Description: Read the configuration file base class in the system Attributes: -- Gitee From c24d71fe63a806170355e03bd00a9d78648618e0 Mon Sep 17 00:00:00 2001 From: MementoMori <1003350679@qq.com> Date: Wed, 8 Jul 2020 10:46:14 +0800 Subject: [PATCH 66/72] add Annotate --- packageship/packageship/application/initsystem/data_import.py | 2 +- packageship/packageship/libs/dbutils/sqlalchemy_helper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index aaed2d8e..eacb9b6c 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -58,7 +58,7 @@ class InitDataBase(): if self.db_type not in ['mysql', 'sqlite']: LOGGER.logger.error("database type configuration error") - raise Exception('database type configuration error') + raise Error('database type configuration error') def __read_config_file(self): """ diff --git a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py index 9662b0c1..407ad2de 100644 --- a/packageship/packageship/libs/dbutils/sqlalchemy_helper.py +++ b/packageship/packageship/libs/dbutils/sqlalchemy_helper.py @@ -44,7 +44,7 @@ class DBHelper(): if self.user_name is None: self.user_name = self._readconfig.get_database('user_name') - self.password = password + self.password = passwrod if self.password is None: self.password = self._readconfig.get_database('password') -- Gitee From db8459258ce4ccbc9a026ef1a18d104d8fd97137 Mon Sep 17 00:00:00 2001 From: jiang Date: Wed, 8 Jul 2020 15:16:58 +0800 Subject: [PATCH 67/72] Modify notes as required --- .../packageship/application/app_global.py | 15 ++-- .../apps/package/function/constants.py | 19 ++-- .../apps/package/function/packages.py | 86 ++++++++++++------- .../application/apps/package/serialize.py | 55 ++++++++---- .../application/apps/package/url.py | 3 +- .../packageship/application/models/package.py | 18 ++-- .../application/models/temporarydb.py | 36 ++++---- .../packageship/application/settings.py | 42 +++++---- packageship/packageship/manage.py | 9 +- packageship/packageship/selfpkg.py | 9 +- packageship/packageship/system_config.py | 6 +- 11 files changed, 181 insertions(+), 117 deletions(-) diff --git a/packageship/packageship/application/app_global.py b/packageship/packageship/application/app_global.py index 726b1b96..ea1d4127 100644 --- a/packageship/packageship/application/app_global.py +++ b/packageship/packageship/application/app_global.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -''' - Interception before request -''' +""" +Description: Interception before request +""" from flask import request from packageship.application import OPERATION from packageship.application.apps.package.url import urls @@ -11,9 +11,12 @@ __all__ = ['identity_verification'] def identity_verification(): - ''' - Requested authentication - ''' + """ + Description: Requested authentication + Args: + Returns: + Raises: + """ if request.url_rule: url_rule = request.url_rule.rule for view, url, authentication in urls: diff --git a/packageship/packageship/application/apps/package/function/constants.py b/packageship/packageship/application/apps/package/function/constants.py index 241855f0..7efe4127 100644 --- a/packageship/packageship/application/apps/package/function/constants.py +++ b/packageship/packageship/application/apps/package/function/constants.py @@ -1,16 +1,19 @@ -''' - Response contain and code ID -''' +#!/usr/bin/python3 +""" +Description: Response contain and code ID +class: ListNode, ResponseCode +""" -class ListNode: +class ListNode(): """ - the structure of dict: + Description: Dethe structure of dict: {package_name: [source_name, dbname, [[parent_node_1, depend_type],[parent_node_2, depend_type],...]], check_tag] } + changeLog: """ SOURCE_NAME = 0 @@ -26,8 +29,8 @@ class ListNode: class ResponseCode(): """ - Description: response code to web - changeLog: + Description: response code to web + changeLog: """ # Four digits are common status codes SUCCESS = "2001" @@ -66,7 +69,7 @@ class ResponseCode(): @classmethod def response_json(cls, code, data=None): """ - classmethod + Description: classmethod """ return { "code": code, diff --git a/packageship/packageship/application/apps/package/function/packages.py b/packageship/packageship/application/apps/package/function/packages.py index 25a0508e..7d3ca455 100644 --- a/packageship/packageship/application/apps/package/function/packages.py +++ b/packageship/packageship/application/apps/package/function/packages.py @@ -1,5 +1,8 @@ +#!/usr/bin/python3 """ -docstring:Get package information and modify package information +Description: Get package information and modify package information +functions: get_packages, buildep_packages, sub_packages, get_single_package, + update_single_package, update_maintaniner_info """ from flask import current_app @@ -13,11 +16,16 @@ from packageship.libs.exception import Error def get_packages(dbname): - ''' + """ Description: Get all packages info - :param dbname: Database name - :return: package info - ''' + Args: + dbname: Database name + Returns: + Package information is returned as a list + Raises: + AttributeError: Object does not have this property + Error: Abnormal error + """ with DBHelper(db_name=dbname) as db_name: src_pack_queryset = db_name.session.query(src_pack).all() resp_list = [] @@ -39,9 +47,13 @@ def get_packages(dbname): def buildep_packages(dbname, src_pack_id): """ Description: Query package layer 1 compilation dependency - :param dbname: databases name - :param src_pack_id: srcpackage id - :return: buildDep Compile dependencies of source packages + Args: + dbname: databases name + src_pack_id: The ID of the source package + Returns: + buildDep Compile dependencies of source packages + Raises: + AttributeError: Object does not have this property """ with DBHelper(db_name=dbname) as db_name: b_pack_requires_set = db_name.session.query( @@ -61,10 +73,14 @@ def buildep_packages(dbname, src_pack_id): def sub_packages(dbname, src_pack_id): """ Description: Query package layer 1 installation dependency - :param dbname: databases name - :param src_pack_id: srcpackage id - :return: subpack Source package to binary package, then find the installation dependencies + Args: + dbname: databases name + src_pack_id: srcpackage id + Returns: + subpack Source package to binary package, then find the installation dependencies of the binary package + Raises: + AttributeError: Object does not have this property """ with DBHelper(db_name=dbname) as db_name: subpack = {} @@ -93,12 +109,16 @@ def sub_packages(dbname, src_pack_id): def get_single_package(dbname, sourcename): - ''' + """ Description: Get all packages info - :param dbname: Database name - :param sourcename: Source package name - :return: package info - ''' + Args: + dbname: Database name + sourcename: Source package name + Returns: + package info + Raises: + AttributeError: Object does not have this property + """ with DBHelper(db_name=dbname) as db_name: package = {} src_pack_obj = db_name.session.query(src_pack).filter_by( @@ -126,11 +146,16 @@ def update_single_package( maintain_level): """ Description: change single package management - :param package_name: package name - :param dbname: Database name - :param maintainer: maintainer info - :param maintain_level: maintain_level info - :return: message success or failed + Args: + package_name: package name + dbname: Database name + maintainer: maintainer info + maintain_level: maintain_level info + Returns: + message success or failed + Raises: + AttributeError: Object does not have this property + TypeError: Abnormal error """ with DBHelper(db_name=dbname) as db_name: update_obj = db_name.session.query( @@ -144,16 +169,19 @@ def update_maintaniner_info(package_name, dbname, maintaniner, maintainlevel): - ''' """ - update separately maintaniner info - :param package_name: package name - :param dbname: Database name - :param maintainer: maintainer info - :param maintain_level: maintain_level info - :return: message success or failed + Description: update separately maintaniner info + Args: + package_name: package name + dbname: Database name + maintainer: maintainer info + maintain_level: maintain_level info + Returns: + message success or failed + Raises: + AttributeError: Object does not have this property + Error: Abnormal error """ - ''' with DBHelper(db_name=dbname) as db_name: src_pack_obj = db_name.session.query(src_pack).filter_by( name=package_name).first() diff --git a/packageship/packageship/application/apps/package/serialize.py b/packageship/packageship/application/apps/package/serialize.py index dbc4735d..ac86ce9a 100644 --- a/packageship/packageship/application/apps/package/serialize.py +++ b/packageship/packageship/application/apps/package/serialize.py @@ -1,5 +1,6 @@ +#!/usr/bin/python3 """ -marshmallow serialize +Description: marshmallow serialize """ from marshmallow import Schema from marshmallow import fields @@ -9,7 +10,7 @@ from marshmallow import validate class PackagesSchema(Schema): """ - PackagesSchema serialize + Description: PackagesSchema serialize """ dbName = fields.Str(validate=validate.Length( max=50), required=False, allow_none=True) @@ -17,7 +18,7 @@ class PackagesSchema(Schema): class GetpackSchema(Schema): """ - GetpackSchema serialize + Description: GetpackSchema serialize """ sourceName = fields.Str( required=True, @@ -32,7 +33,13 @@ class GetpackSchema(Schema): def validate_maintainlevel(maintainlevel): """ - Method test + Description: Method test + Args: + maintainlevel: maintainlevel + Returns: + True or failure + Raises: + ValidationError: Test failed """ if maintainlevel not in ['1', '2', '3', '4']: raise ValidationError("maintainLevel is illegal data ") @@ -40,7 +47,7 @@ def validate_maintainlevel(maintainlevel): class PutpackSchema(Schema): """ - PutpackSchema serialize + Description: PutpackSchema serialize """ sourceName = fields.Str( required=True, @@ -62,7 +69,7 @@ class PutpackSchema(Schema): class InstallDependSchema(Schema): """ - InstallDependSchema + Description: InstallDependSchema """ binaryName = fields.Str( required=True, @@ -73,7 +80,7 @@ class InstallDependSchema(Schema): class BuildDependSchema(Schema): """ - BuildDependSchema serialize + Description: BuildDependSchema serialize """ sourceName = fields.Str( required=True, @@ -84,7 +91,13 @@ class BuildDependSchema(Schema): def validate_withsubpack(withsubpack): """ - Method test + Description: Method test + Args: + withsubpack: withsubpack + Returns: + True or failure + Raises: + ValidationError: Test failed """ if withsubpack not in ['0', '1']: raise ValidationError("withSubpack is illegal data ") @@ -92,7 +105,7 @@ def validate_withsubpack(withsubpack): class BeDependSchema(Schema): """ - BeDependSchema serialize + Description: BeDependSchema serialize """ packagename = fields.Str( required=True, @@ -111,7 +124,7 @@ class BeDependSchema(Schema): def validate_selfbuild(selfbuild): """ - Method test + Description: Method test """ if selfbuild not in ['0', '1']: raise ValidationError("selfbuild is illegal data ") @@ -119,7 +132,7 @@ def validate_selfbuild(selfbuild): def validate_packtype(packtype): """ - Method test + Description: Method test """ if packtype not in ['source', 'binary']: raise ValidationError("packtype is illegal data ") @@ -127,7 +140,7 @@ def validate_packtype(packtype): class SelfDependSchema(Schema): """ - SelfDependSchema serialize + Description: SelfDependSchema serialize """ packagename = fields.Str( required=True, @@ -145,7 +158,7 @@ class SelfDependSchema(Schema): class DeletedbSchema(Schema): """ - DeletedbSchema serialize + Description: DeletedbSchema serialize """ dbName = fields.Str( required=True, @@ -155,17 +168,21 @@ class DeletedbSchema(Schema): def have_err_db_name(db_list, db_priority): - ''' - @param:db_list db list of inputs - @param:db_priority default list - return:If any element in db_list is no longer in db_priority, return false - ''' + """ + Description: have error database name method + Args: + db_list: db_list db list of inputs + db_priority: db_priority default list + Returns: + If any element in db_list is no longer in db_priority, return false + Raises: + """ return any(filter(lambda db_name: db_name not in db_priority, db_list)) class InitSystemSchema(Schema): """ - InitSystemSchema serialize + Description: InitSystemSchema serialize """ configfile = fields.Str( validate=validate.Length( diff --git a/packageship/packageship/application/apps/package/url.py b/packageship/packageship/application/apps/package/url.py index 1e12fa78..5a082134 100644 --- a/packageship/packageship/application/apps/package/url.py +++ b/packageship/packageship/application/apps/package/url.py @@ -1,5 +1,6 @@ +#!/usr/bin/python3 """ - url set +Description: url set """ from . import view diff --git a/packageship/packageship/application/models/package.py b/packageship/packageship/application/models/package.py index 95e62a6a..62f7ec18 100644 --- a/packageship/packageship/application/models/package.py +++ b/packageship/packageship/application/models/package.py @@ -1,17 +1,17 @@ #!/usr/bin/python3 -''' -Database entity model mapping -''' +""" +Description: Database entity model mapping +""" from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship from packageship.libs.dbutils.sqlalchemy_helper import DBHelper class src_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - functional description:Source package model + """ + Description: functional description:Source package model modify record: - ''' + """ __tablename__ = 'src_pack' @@ -33,10 +33,10 @@ class src_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 class bin_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - functional description:Binary package data + """ + Description: functional description:Binary package data modify record: - ''' + """ __tablename__ = 'bin_pack' id = Column(Integer, primary_key=True) diff --git a/packageship/packageship/application/models/temporarydb.py b/packageship/packageship/application/models/temporarydb.py index 79d3c7ec..07a2dd17 100644 --- a/packageship/packageship/application/models/temporarydb.py +++ b/packageship/packageship/application/models/temporarydb.py @@ -1,15 +1,15 @@ #!/usr/bin/python3 -''' -Database entity model mapping -''' +""" +Description: Database entity model mapping +""" from sqlalchemy import Column, Integer, String from packageship.libs.dbutils.sqlalchemy_helper import DBHelper class src_package(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - Temporary source package model - ''' + """ + Description: Temporary source package model + """ __tablename__ = 'src_package' @@ -27,9 +27,9 @@ class src_package(DBHelper.BASE): # pylint: disable=C0103,R0903 class bin_package(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - Temporary binary package model - ''' + """ + Description: Temporary binary package model + """ __tablename__ = 'bin_package' pkgKey = Column(Integer, primary_key=True) @@ -48,9 +48,9 @@ class bin_package(DBHelper.BASE): # pylint: disable=C0103,R0903 class src_requires(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - Temporary source package depends on package model - ''' + """ + Description: Temporary source package depends on package model + """ __tablename__ = 'src_requires' id = Column(Integer, primary_key=True) @@ -61,9 +61,9 @@ class src_requires(DBHelper.BASE): # pylint: disable=C0103,R0903 class bin_requiresment(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - Dependency package model for temporary binary packages - ''' + """ + Description: Dependency package model for temporary binary packages + """ __tablename__ = 'bin_requiresment' id = Column(Integer, primary_key=True) @@ -74,9 +74,9 @@ class bin_requiresment(DBHelper.BASE): # pylint: disable=C0103,R0903 class bin_provides(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' - Provided package model for temporary binary packages - ''' + """ + Description: Provided package model for temporary binary packages + """ __tablename__ = 'bin_provides' id = Column(Integer, primary_key=True) diff --git a/packageship/packageship/application/settings.py b/packageship/packageship/application/settings.py index 41fac74f..bc090439 100644 --- a/packageship/packageship/application/settings.py +++ b/packageship/packageship/application/settings.py @@ -1,15 +1,18 @@ #!/usr/bin/python3 -''' - Basic configuration of flask framework -''' +""" +Description: Basic configuration of flask framework +""" import random from packageship.libs.configutils.readconfig import ReadConfig class Config(): - ''' - Configuration items in a formal environment - ''' + """ + Description: Configuration items in a formal environment + Attributes: + _read_config: read config + _set_config_val: Set the value of the configuration item + """ SECRET_KEY = None DEBUG = False @@ -24,31 +27,34 @@ class Config(): @classmethod def _random_secret_key(cls, random_len=32): - ''' - Generate random strings - ''' + """ + Description: Generate random strings + """ cls.SECRET_KEY = ''.join( [random.choice('abcdefghijklmnopqrstuvwxyz!@#$%^&*()') for index in range(random_len)]) @classmethod def _set_debug(cls, debug): - ''' - Set the debugging mode - ''' + """ + Description: Set the debugging mode + """ if debug == 'true': cls.DEBUG = True @classmethod def _set_log_level(cls, log_level): - ''' - Set the log level - ''' + """ + Description: Set the log level + """ cls.LOG_LEVEL = log_level def set_config_val(self): - ''' - Set the value of the configuration item - ''' + """ + Description: Set the value of the configuration item + Args: + Returns: + Raises: + """ Config._random_secret_key() debug = self._read_config.get_system('debug') diff --git a/packageship/packageship/manage.py b/packageship/packageship/manage.py index 1cef1912..dc79873e 100644 --- a/packageship/packageship/manage.py +++ b/packageship/packageship/manage.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -''' - Entry for project initialization and service startupc -''' +""" +Description: Entry for project initialization and service startupc +""" import os from packageship.libs.exception import Error try: @@ -28,6 +28,9 @@ else: @app.before_request def before_request(): + """ + Description: Global request interception + """ if not identity_verification(): return 'No right to perform operation' diff --git a/packageship/packageship/selfpkg.py b/packageship/packageship/selfpkg.py index 7a14a9a6..b5878168 100644 --- a/packageship/packageship/selfpkg.py +++ b/packageship/packageship/selfpkg.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -''' - Entry for project initialization and service startupc -''' +""" +Description: Entry for project initialization and service startupc +""" import os from flask_script import Manager from packageship.libs.exception import Error @@ -29,6 +29,9 @@ else: @app.before_request def before_request(): + """ + Description: Global request interception + """ if not identity_verification(): return 'No right to perform operation' diff --git a/packageship/packageship/system_config.py b/packageship/packageship/system_config.py index df2dc142..14de44a9 100644 --- a/packageship/packageship/system_config.py +++ b/packageship/packageship/system_config.py @@ -1,8 +1,8 @@ #!/usr/bin/python3 -''' -System-level file configuration, mainly configure +""" +Description:System-level file configuration, mainly configure the address of the operating environment, commonly used variables, etc. -''' +""" import os import sys -- Gitee From 72b67c70df11fbb9c2dac137fad7d35be2520e82 Mon Sep 17 00:00:00 2001 From: stutao <1450026690@qq.com> Date: Wed, 8 Jul 2020 18:58:55 +0800 Subject: [PATCH 68/72] Add files to ensure that all unit tests can be run in a single file. --- .../packageship/application/app_global.py | 6 +- .../application/apps/package/__init__.py | 4 +- .../application/initsystem/data_import.py | 19 ++- .../application/initsystem/datamerge.py | 10 +- .../test/base_code/common_test_code.py | 3 +- .../test/base_code/init_config_path.py | 15 ++- packageship/test/base_code/my_test_runner.py | 67 ++++++++++ .../test/base_code/operate_data_base.py | 11 +- packageship/test/base_code/read_data_base.py | 6 +- packageship/test/run_init_system_tests.py | 12 -- packageship/test/run_read_tests.py | 24 ---- packageship/test/run_tests.py | 83 ++++++++++++ packageship/test/run_write_test.py | 14 -- .../dependent_query_tests/test_be_depend.py | 21 +-- .../test_build_depend.py | 31 ++--- .../test_install_depend.py | 29 ++++- .../dependent_query_tests/test_self_depend.py | 40 +++--- .../init_system_tests/test_importdata.py | 122 ++++++++---------- .../packages_tests/test_packages.py | 13 +- .../repodatas_test/test_delete_repodatas.py | 15 +-- .../repodatas_test/test_get_repodatas.py | 9 +- .../test_get_singlepack.py | 11 +- .../test_update_singlepack.py | 12 +- 23 files changed, 326 insertions(+), 251 deletions(-) create mode 100644 packageship/test/base_code/my_test_runner.py delete mode 100644 packageship/test/run_init_system_tests.py delete mode 100644 packageship/test/run_read_tests.py create mode 100644 packageship/test/run_tests.py delete mode 100644 packageship/test/run_write_test.py diff --git a/packageship/packageship/application/app_global.py b/packageship/packageship/application/app_global.py index ea1d4127..25d9dbe0 100644 --- a/packageship/packageship/application/app_global.py +++ b/packageship/packageship/application/app_global.py @@ -3,7 +3,7 @@ Description: Interception before request """ from flask import request -from packageship.application import OPERATION +from packageship import application from packageship.application.apps.package.url import urls @@ -20,8 +20,8 @@ def identity_verification(): if request.url_rule: url_rule = request.url_rule.rule for view, url, authentication in urls: - if url == url_rule and OPERATION in authentication.keys(): - if request.method not in authentication.get(OPERATION): + if url == url_rule and application.OPERATION in authentication.keys(): + if request.method not in authentication.get(application.OPERATION): return False break return True diff --git a/packageship/packageship/application/apps/package/__init__.py b/packageship/packageship/application/apps/package/__init__.py index d054951e..987ad614 100644 --- a/packageship/packageship/application/apps/package/__init__.py +++ b/packageship/packageship/application/apps/package/__init__.py @@ -1,7 +1,7 @@ from flask.blueprints import Blueprint from flask_restful import Api from packageship.application.apps.package.url import urls -from packageship.application import OPERATION +from packageship import application package = Blueprint('package', __name__) @@ -9,7 +9,7 @@ package = Blueprint('package', __name__) api = Api() for view, url, operation in urls: - if OPERATION and OPERATION in operation.keys(): + if application.OPERATION and application.OPERATION in operation.keys(): api.add_resource(view, url) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index 1cbdfbc9..4732f13c 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -22,8 +22,7 @@ from packageship.application.models.temporarydb import src_requires from packageship.application.models.temporarydb import bin_package from packageship.application.models.temporarydb import bin_requiresment from packageship.application.models.temporarydb import bin_provides -from packageship.system_config import DATABASE_FILE_INFO -from packageship.system_config import DATABASE_FOLDER_PATH +from packageship import system_config LOGGER = Log(__name__) @@ -606,9 +605,9 @@ class InitDataBase(): IOError: File or network operation io abnormal """ try: - if not os.path.exists(DATABASE_FILE_INFO): - pathlib.Path(DATABASE_FILE_INFO).touch() - with open(DATABASE_FILE_INFO, 'a+', encoding='utf8') as file_context: + if not os.path.exists(system_config.DATABASE_FILE_INFO): + pathlib.Path(system_config.DATABASE_FILE_INFO).touch() + with open(system_config.DATABASE_FILE_INFO, 'a+', encoding='utf8') as file_context: setting_content = [] if 'database_content' in Kwargs.keys(): content = Kwargs.get('database_content') @@ -634,8 +633,8 @@ class InitDataBase(): """ try: - if os.path.exists(DATABASE_FILE_INFO): - os.remove(DATABASE_FILE_INFO) + if os.path.exists(system_config.DATABASE_FILE_INFO): + os.remove(system_config.DATABASE_FILE_INFO) except (IOError, Error) as exception_msg: LOGGER.logger.error(exception_msg) return False @@ -661,14 +660,14 @@ class InitDataBase(): if del_result: try: - file_read = open(DATABASE_FILE_INFO, 'r', encoding='utf-8') + file_read = open(system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8') _databases = yaml.load( file_read.read(), Loader=yaml.FullLoader) for database in _databases: if database.get('database_name') == db_name: _databases.remove(database) # Delete the successfully imported database configuration node - with open(DATABASE_FILE_INFO, 'w+', encoding='utf-8') as file_context: + with open(system_config.DATABASE_FILE_INFO, 'w+', encoding='utf-8') as file_context: yaml.safe_dump(_databases, file_context) except (IOError, Error) as del_config_error: LOGGER.logger.error(del_config_error) @@ -849,7 +848,7 @@ class SqliteDatabaseOperations(): self.database_file_folder = self._read_config.get_system( 'data_base_path') if not self.database_file_folder: - self.database_file_folder = DATABASE_FOLDER_PATH + self.database_file_folder = system_config.DATABASE_FOLDER_PATH if not os.path.exists(self.database_file_folder): try: diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index 3123d1a3..a4837457 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -35,11 +35,11 @@ class MergeData(): self.db_file = db_file self.db_type = 'sqlite:///' self.datum_database = 'maintenance.information' - self.src_requires_dicts = None - self.src_package_datas = None - self.bin_provides_dicts = None - self.bin_package_datas = None - self.mainter_infos = None + self.src_requires_dicts = dict() + self.src_package_datas = [] + self.bin_provides_dicts = dict() + self.bin_package_datas = [] + self.mainter_infos = dict() @staticmethod def __columns(cursor): diff --git a/packageship/test/base_code/common_test_code.py b/packageship/test/base_code/common_test_code.py index 39ce8db0..f92cb9d8 100644 --- a/packageship/test/base_code/common_test_code.py +++ b/packageship/test/base_code/common_test_code.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ Compare the values in two Python data types for equality, ignoring the order of values @@ -38,7 +39,7 @@ def get_correct_json_by_filename(filename): "test", "common_files", "correct_test_result_json", - f"{filename}.json") + "{}.json".format(filename)) try: with open(json_path, "r") as json_fp: correct_list = json.loads(json_fp.read()) diff --git a/packageship/test/base_code/init_config_path.py b/packageship/test/base_code/init_config_path.py index 82103fd4..19dff752 100644 --- a/packageship/test/base_code/init_config_path.py +++ b/packageship/test/base_code/init_config_path.py @@ -1,5 +1,8 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- - +""" +InitConf +""" import os from configparser import ConfigParser from packageship import system_config @@ -7,6 +10,10 @@ import yaml class InitConf: + """ + InitConf + """ + def __init__(self): base_path = os.path.join(os.path.dirname(system_config.BASE_PATH), "test", @@ -23,8 +30,10 @@ class InitConf: origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) for index, obj in enumerate(origin_yaml, 1): - src_path = os.path.join(base_path, "db_origin", f"data_{str(index)}_src.sqlite") - bin_path = os.path.join(base_path, "db_origin", f"data_{str(index)}_bin.sqlite") + src_path = os.path.join(base_path, "db_origin", + "data_{}_src.sqlite".format(str(index))) + bin_path = os.path.join(base_path, "db_origin", + "data_{}_bin.sqlite".format(str(index))) obj["src_db_file"] = [src_path] obj["bin_db_file"] = [bin_path] with open(conf_path, 'w', encoding='utf-8') as w_f: diff --git a/packageship/test/base_code/my_test_runner.py b/packageship/test/base_code/my_test_runner.py new file mode 100644 index 00000000..e3ce22f3 --- /dev/null +++ b/packageship/test/base_code/my_test_runner.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 +""" +Inherited from unittest.TestResult, +The simple statistical function is realized. +""" +import sys +import unittest + + +class MyTestResult(unittest.TestResult): + """ + Inherited from unittest.TestResult, + The simple statistical function is realized. + """ + + def __init__(self, verbosity=0): + super(MyTestResult, self).__init__() + self.success_case_count = 0 + self.err_case_count = 0 + self.failure_case_count = 0 + self.verbosity = verbosity + + def addSuccess(self, test): + """When the use case is executed successfully""" + self.success_case_count += 1 + super(MyTestResult, self).addSuccess(test) + if self.verbosity > 0: + sys.stderr.write('Success ') + sys.stderr.write(str(test)) + sys.stderr.write('\n') + + def addError(self, test, err): + """When a code error causes a use case to fail""" + self.err_case_count += 1 + super(MyTestResult, self).addError(test, err) + if self.verbosity > 0: + sys.stderr.write('Error ') + sys.stderr.write(str(test)+'\n') + _,err_info = self.errors[-1] + sys.stderr.write(err_info) + sys.stderr.write('\n') + + def addFailure(self, test, err): + """When the assertion is false""" + self.failure_case_count += 1 + super(MyTestResult, self).addFailure(test, err) + if self.verbosity > 0: + sys.stderr.write('Failure ') + sys.stderr.write(str(test)+'\n') + _, err_info = self.failures[-1] + sys.stderr.write(err_info) + sys.stderr.write('\n') + + +class MyTestRunner(): + """ + Run All TestCases + """ + + def __init__(self, verbosity=0): + self.verbosity = verbosity + + def run(self, test): + """run MyTestResult and return result""" + result = MyTestResult(self.verbosity) + test(result) + return result diff --git a/packageship/test/base_code/operate_data_base.py b/packageship/test/base_code/operate_data_base.py index c3d18e82..b5a472a9 100644 --- a/packageship/test/base_code/operate_data_base.py +++ b/packageship/test/base_code/operate_data_base.py @@ -1,7 +1,12 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- +""" +OperateTestBase +""" import os import unittest +from packageship.libs.exception import Error try: from packageship import system_config @@ -21,14 +26,14 @@ try: from test.base_code.init_config_path import init_config from packageship.manage import app -except Exception as e: +except Error: raise class OperateTestBase(unittest.TestCase): """ - + OperateTestBase """ - def setUp(self) -> None: + def setUp(self): self.client = app.test_client() diff --git a/packageship/test/base_code/read_data_base.py b/packageship/test/base_code/read_data_base.py index d183d44d..0e7a33dd 100644 --- a/packageship/test/base_code/read_data_base.py +++ b/packageship/test/base_code/read_data_base.py @@ -1,7 +1,9 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- import os import unittest +from packageship.libs.exception import Error try: from packageship import system_config @@ -22,10 +24,10 @@ try: from test.base_code.init_config_path import init_config from packageship.selfpkg import app -except Exception as e: +except Error: raise class ReadTestBase(unittest.TestCase): - def setUp(self) -> None: + def setUp(self): self.client = app.test_client() diff --git a/packageship/test/run_init_system_tests.py b/packageship/test/run_init_system_tests.py deleted file mode 100644 index 203654cf..00000000 --- a/packageship/test/run_init_system_tests.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding:utf-8 -*- -"""Test case of init data""" -from test.test_module.init_system_tests.test_importdata import test_import_data_suit - - -def execute_init(): - """Test case of init data""" - test_import_data_suit() - - -if __name__ == '__main__': - execute_init() diff --git a/packageship/test/run_read_tests.py b/packageship/test/run_read_tests.py deleted file mode 100644 index 39c41a5b..00000000 --- a/packageship/test/run_read_tests.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding:utf-8 -*- -""" -Test case of reading data -""" -from test.test_module.dependent_query_tests.test_build_depend import test_build_depend_suit -from test.test_module.packages_tests.test_packages import test_packages_suit -from test.test_module.single_package_tests.test_get_singlepack import test_get_single_package_suit -from test.test_module.repodatas_test.test_get_repodatas import test_get_repodatas_suit -from test.test_module.dependent_query_tests.test_be_depend import test_be_depend_suit -from test.test_module.dependent_query_tests.test_install_depend import test_install_depend_suit -from test.test_module.dependent_query_tests.test_self_depend import test_self_depend_suit - -def execute_read(): - """Test case of reading data""" - test_build_depend_suit() - test_packages_suit() - test_get_single_package_suit() - test_get_repodatas_suit() - test_be_depend_suit() - test_install_depend_suit() - test_self_depend_suit() - -if __name__ == '__main__': - execute_read() diff --git a/packageship/test/run_tests.py b/packageship/test/run_tests.py new file mode 100644 index 00000000..6d389d6b --- /dev/null +++ b/packageship/test/run_tests.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 +# -*- coding:utf-8 -*- +""" +Execute all test cases +""" +import unittest +import datetime +from test.base_code.my_test_runner import MyTestRunner + +RUNNER = MyTestRunner(verbosity=1) + + +def import_data_tests(): + """Initialize related test cases""" + + from test.test_module.init_system_tests.test_importdata import ImportData + suite = unittest.TestSuite() + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ImportData)) + + return RUNNER.run(suite) + + +def read_data_tests(): + """Test cases with read operations on data""" + + from test.test_module.dependent_query_tests.test_install_depend import TestInstallDepend + from test.test_module.dependent_query_tests.test_self_depend import TestSelfDepend + from test.test_module.dependent_query_tests.test_be_depend import TestBeDepend + from test.test_module.repodatas_test.test_get_repodatas import TestGetRepodatas + from test.test_module.dependent_query_tests.test_build_depend import TestBuildDepend + from test.test_module.packages_tests.test_packages import TestPackages + from test.test_module.single_package_tests.test_get_singlepack import TestGetSinglePack + suite = unittest.TestSuite() + + classes = [TestInstallDepend, TestSelfDepend, TestBeDepend, + TestGetRepodatas, TestBuildDepend, TestPackages, TestGetSinglePack] + + for cls in classes: + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(cls)) + return RUNNER.run(suite) + + +def write_data_tests(): + """Test cases with write operations on data""" + + from test.test_module.repodatas_test.test_delete_repodatas import TestDeleteRepodatas + from test.test_module.single_package_tests.test_update_singlepack import TestUpdatePackage + suite = unittest.TestSuite() + + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDeleteRepodatas)) + suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestUpdatePackage)) + + return RUNNER.run(suite) + + +def main(): + """Test case execution entry function""" + + start_time = datetime.datetime.now() + + result_4_import = import_data_tests() + result_4_read = read_data_tests() + result_4_write = write_data_tests() + + stop_time = datetime.datetime.now() + + print('\nA total of %s test cases were run: \nsuccessful:%s\tfailed:%s\terror:%s\n' % ( + int(result_4_import.testsRun) + int(result_4_read.testsRun) + int(result_4_write.testsRun), + int( + result_4_import.success_case_count + ) + int(result_4_read.success_case_count) + int(result_4_write.success_case_count), + int( + result_4_import.failure_case_count + ) + int(result_4_read.failure_case_count) + int(result_4_write.failure_case_count), + int( + result_4_import.err_case_count + ) + int(result_4_read.err_case_count) + int(result_4_write.err_case_count) + )) + + print('Total Time: %s' % (stop_time - start_time)) + + +main() diff --git a/packageship/test/run_write_test.py b/packageship/test/run_write_test.py deleted file mode 100644 index 8c6947df..00000000 --- a/packageship/test/run_write_test.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding:utf-8 -*- -"""Test case of writing data""" -from test.test_module.repodatas_test.test_delete_repodatas import test_delete_repodatas_suit -from test.test_module.single_package_tests.test_update_singlepack import test_updata_single_package_suit - - -def execute_operate(): - """Test case of writing data""" - test_updata_single_package_suit() - test_delete_repodatas_suit() - - -if __name__ == '__main__': - execute_operate() diff --git a/packageship/test/test_module/dependent_query_tests/test_be_depend.py b/packageship/test/test_module/dependent_query_tests/test_be_depend.py index 0c43cb2c..57241236 100644 --- a/packageship/test/test_module/dependent_query_tests/test_be_depend.py +++ b/packageship/test/test_module/dependent_query_tests/test_be_depend.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ Less transmission is always parameter transmission @@ -8,10 +9,11 @@ from test.base_code.read_data_base import ReadTestBase from test.base_code.common_test_code import compare_two_values, get_correct_json_by_filename from packageship.application.apps.package.function.constants import ResponseCode + class TestBeDepend(ReadTestBase): - ''' + """ The dependencies of the package are tested - ''' + """ def test_lack_parameter(self): """ @@ -355,18 +357,3 @@ class TestBeDepend(ReadTestBase): resp_dict = json.loads(resp.data) self.assertTrue(compare_two_values(output_for_input, resp_dict), msg="The answer is not correct") - -def test_be_depend_suit(): - """ - Start the test case function - """ - print("---TestBeDepend START---") - suite = unittest.TestSuite() - suite.addTest(TestBeDepend("test_lack_parameter")) - suite.addTest(TestBeDepend("test_wrong_parameter")) - suite.addTest(TestBeDepend("test_true_params_result")) - unittest.TextTestRunner().run(suite) - - -# if __name__ == '__main__': -# unittest.main() diff --git a/packageship/test/test_module/dependent_query_tests/test_build_depend.py b/packageship/test/test_module/dependent_query_tests/test_build_depend.py index 31ba5b52..5e0df7ff 100644 --- a/packageship/test/test_module/dependent_query_tests/test_build_depend.py +++ b/packageship/test/test_module/dependent_query_tests/test_build_depend.py @@ -1,7 +1,8 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- -''' +""" build_depend unittest -''' +""" import json import unittest @@ -11,15 +12,15 @@ from packageship.application.apps.package.function.constants import ResponseCode class TestBuildDepend(ReadTestBase): - ''' + """ class for test build_depend - ''' + """ def test_empty_source_name_dblist(self): - ''' + """ test empty parameters:sourceName,dbList :return: - ''' + """ resp = self.client.post("/packages/findBuildDepend", data="{}", content_type="application/json") @@ -57,10 +58,10 @@ class TestBuildDepend(ReadTestBase): self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") def test_wrong_source_name_dblist(self): - ''' + """ test wrong parameters:sourceName,dbList :return: None - ''' + """ resp = self.client.post("/packages/findBuildDepend", data=json.dumps({"sourceName": 0}), content_type="application/json") @@ -136,6 +137,11 @@ class TestBuildDepend(ReadTestBase): self.assertIsNone(resp_dict.get("data"), msg="Error in data information return") def test_true_params_result(self): + """ + test_true_params_result + Returns: + + """ correct_list = get_correct_json_by_filename("build_depend") self.assertNotEqual([], correct_list, msg="Error reading JSON file") @@ -151,14 +157,5 @@ class TestBuildDepend(ReadTestBase): msg="The answer is not correct") -def test_build_depend_suit(): - print("---TestBuildDepend START---") - suite = unittest.TestSuite() - suite.addTest(TestBuildDepend("test_empty_source_name_dblist")) - suite.addTest(TestBuildDepend("test_wrong_source_name_dblist")) - suite.addTest(TestBuildDepend("test_true_params_result")) - unittest.TextTestRunner().run(suite) - - if __name__ == '__main__': unittest.main() diff --git a/packageship/test/test_module/dependent_query_tests/test_install_depend.py b/packageship/test/test_module/dependent_query_tests/test_install_depend.py index a48bf38e..81ddf98b 100644 --- a/packageship/test/test_module/dependent_query_tests/test_install_depend.py +++ b/packageship/test/test_module/dependent_query_tests/test_install_depend.py @@ -1,4 +1,8 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- +""" +TestInstallDepend +""" import unittest import json @@ -8,10 +12,16 @@ from packageship.application.apps.package.function.constants import ResponseCode class TestInstallDepend(ReadTestBase): - + """ + TestInstallDepend + """ def test_empty_binaryName_dbList(self): + """ + test_empty_binaryName_dbList + Returns: + """ resp = self.client.post("/packages/findInstallDepend", data="{}", content_type="application/json") @@ -49,6 +59,11 @@ class TestInstallDepend(ReadTestBase): self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") def test_wrong_binaryName_dbList(self): + """ + test_empty_binaryName_dbList + Returns: + + """ resp = self.client.post("/packages/findInstallDepend", data=json.dumps({"binaryName": 0}), content_type="application/json") @@ -125,6 +140,11 @@ class TestInstallDepend(ReadTestBase): self.assertIsNone(resp_dict.get("data"), msg="Error in data information return") def test_true_params_result(self): + """ + test_empty_binaryName_dbList + Returns: + + """ correct_list = get_correct_json_by_filename("install_depend") self.assertNotEqual([], correct_list, msg="Error reading JSON file") @@ -138,12 +158,7 @@ class TestInstallDepend(ReadTestBase): resp_dict = json.loads(resp.data) self.assertTrue(compare_two_values(output_for_input, resp_dict), msg="The answer is not correct") -def test_install_depend_suit(): - suite = unittest.TestSuite() - suite.addTest(TestInstallDepend("test_empty_binaryName_dbList")) - suite.addTest(TestInstallDepend("test_wrong_binaryName_dbList")) - suite.addTest(TestInstallDepend("test_true_params_result")) - unittest.TextTestRunner().run(suite) + if __name__ == '__main__': unittest.main() diff --git a/packageship/test/test_module/dependent_query_tests/test_self_depend.py b/packageship/test/test_module/dependent_query_tests/test_self_depend.py index 916d453e..4a2fcb5d 100644 --- a/packageship/test/test_module/dependent_query_tests/test_self_depend.py +++ b/packageship/test/test_module/dependent_query_tests/test_self_depend.py @@ -1,4 +1,8 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- +""" +TestSelfDepend +""" import unittest import json @@ -9,8 +13,16 @@ from packageship.application.apps.package.function.searchdb import db_priority class TestSelfDepend(ReadTestBase): + """ + TestSelfDepend + """ def test_empty_parameter(self): + """ + test_empty_parameter + Returns: + + """ resp = self.client.post("/packages/findSelfDepend", data='{}', content_type="application/json") @@ -116,6 +128,11 @@ class TestSelfDepend(ReadTestBase): self.assertIsNotNone(resp_dict.get("data"), msg="Data return error!") def test_wrong_parameter(self): + """ + test_wrong_parameter + Returns: + + """ resp = self.client.post("/packages/findSelfDepend", data=json.dumps({ "packagename": "wukong" @@ -248,6 +265,11 @@ class TestSelfDepend(ReadTestBase): self.assertIsNone(resp_dict.get("data"), msg="Data return error!") def test_true_params_result(self): + """ + test_true_params_result + Returns: + + """ correct_list = get_correct_json_by_filename("self_depend") self.assertNotEqual([], correct_list, msg="Error reading JSON file") @@ -259,22 +281,8 @@ class TestSelfDepend(ReadTestBase): content_type="application/json") output_for_input = correct_data["output"] resp_dict = json.loads(resp.data) - if not compare_two_values(output_for_input, resp_dict): - from pprint import pprint - print(input_value) - # pprint(output_for_input) - pprint(resp_dict) - - # self.assertTrue(compare_two_values(output_for_input, resp_dict), - # msg="The answer is not correct") - - -def test_self_depend_suit(): - suite = unittest.TestSuite() - suite.addTest(TestSelfDepend("test_empty_parameter")) - suite.addTest(TestSelfDepend("test_wrong_parameter")) - suite.addTest(TestSelfDepend("test_true_params_result")) - unittest.TextTestRunner().run(suite) + self.assertTrue(compare_two_values(output_for_input, resp_dict), + msg="The answer is not correct") if __name__ == '__main__': diff --git a/packageship/test/test_module/init_system_tests/test_importdata.py b/packageship/test/test_module/init_system_tests/test_importdata.py index dd2906cf..35e303aa 100644 --- a/packageship/test/test_module/init_system_tests/test_importdata.py +++ b/packageship/test/test_module/init_system_tests/test_importdata.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ test import_databases @@ -5,8 +6,11 @@ test import_databases import os import shutil import unittest +import warnings from configparser import ConfigParser +import yaml from packageship import system_config +from packageship.libs.exception import Error try: @@ -29,13 +33,12 @@ try: from test.base_code.init_config_path import init_config -except Exception: - raise -import warnings -import yaml +except Error: + raise Error from packageship.application.initsystem.data_import import InitDataBase -from packageship.libs.exception import ContentNoneException, DatabaseRepeatException +from packageship.libs.exception import ContentNoneException +from packageship.libs.exception import DatabaseRepeatException from packageship.libs.configutils.readconfig import ReadConfig @@ -49,13 +52,12 @@ class ImportData(unittest.TestCase): warnings.filterwarnings("ignore") def test_empty_param(self): - - # If init is not obtained_ conf_ Path parameter + """If init is not obtained_ conf_ Path parameter""" try: InitDataBase(config_file_path=None).init_data() - except Exception as e: + except ContentNoneException as error: self.assertEqual( - e.__class__, + error.__class__, ContentNoneException, msg="No init in package_ conf_ Path parameter, wrong exception type thrown") @@ -69,9 +71,9 @@ class ImportData(unittest.TestCase): w_f.write("") InitDataBase(config_file_path=_config_path).init_data() - except Exception as e: + except ContentNoneException as error: self.assertEqual( - e.__class__, + error.__class__, ContentNoneException, msg="Yaml file exists, but the content is empty. The exception type is wrong") finally: @@ -83,18 +85,18 @@ class ImportData(unittest.TestCase): try: _config_path = ReadConfig().get_system('init_conf_path') shutil.copyfile(_config_path, _config_path + '.bak') - with open(_config_path, 'r', encoding='utf-8') as f: - origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + with open(_config_path, 'r', encoding='utf-8') as file: + origin_yaml = yaml.load(file.read(), Loader=yaml.FullLoader) for obj in origin_yaml: obj["dbname"] = "openEuler" with open(_config_path, 'w', encoding='utf-8') as w_f: yaml.dump(origin_yaml, w_f) InitDataBase(config_file_path=_config_path).init_data() - except Exception as e: + except DatabaseRepeatException as error: self.assertEqual( - e.__class__, + error.__class__, DatabaseRepeatException, msg="Yaml file exists but DB_ Name duplicate exception type is wrong") finally: @@ -103,8 +105,8 @@ class ImportData(unittest.TestCase): os.rename(_config_path + '.bak', _config_path) def test_wrong_param(self): - # If the corresponding current init_ conf_ The directory specified by - # path is incorrect + """If the corresponding current init_ conf_ The directory + specified by path is incorrect""" try: # Back up source files shutil.copyfile( @@ -118,9 +120,9 @@ class ImportData(unittest.TestCase): _config_path = ReadConfig().get_system('init_conf_path') InitDataBase(config_file_path=_config_path).init_data() - except Exception as e: + except FileNotFoundError as error: self.assertEqual( - e.__class__, + error.__class__, FileNotFoundError, msg="init_ conf_ Path specified directory is empty exception type is wrong") finally: @@ -144,10 +146,10 @@ class ImportData(unittest.TestCase): _config_path = ReadConfig().get_system('init_conf_path') InitDataBase(config_file_path=None).init_data() - except Exception as e: + except Error as error: self.assertEqual( - e.__class__, - Exception, + error.__class__, + Error, msg="Wrong exception type thrown when dbtype is wrong") finally: # To restore a file, delete the file first and then rename it back @@ -157,61 +159,60 @@ class ImportData(unittest.TestCase): system_config.SYS_CONFIG_PATH) def test_dbname(self): + """test dbname""" try: _config_path = ReadConfig().get_system('init_conf_path') shutil.copyfile(_config_path, _config_path + '.bak') - with open(_config_path, 'r', encoding='utf-8') as f: - origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + with open(_config_path, 'r', encoding='utf-8') as file: + origin_yaml = yaml.load(file.read(), Loader=yaml.FullLoader) for obj in origin_yaml: obj["dbname"] = "" with open(_config_path, 'w', encoding='utf-8') as w_f: yaml.dump(origin_yaml, w_f) + InitDataBase(config_file_path=_config_path).init_data() - with open(system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: - init_database_date = yaml.load( - file_context.read(), Loader=yaml.FullLoader) + except DatabaseRepeatException as error: + self.assertEqual( - init_database_date, - None, - msg=" Priority must be a positive integer between 0 and 100 ") - except Exception as e: - return + error.__class__, + DatabaseRepeatException, + msg="Yaml file exists but DB_ Name duplicate exception type is wrong") finally: # Restore files os.remove(_config_path) os.rename(_config_path + '.bak', _config_path) def test_src_db_file(self): + """test src db file""" try: _config_path = ReadConfig().get_system('init_conf_path') shutil.copyfile(_config_path, _config_path + '.bak') - with open(_config_path, 'r', encoding='utf-8') as f: - origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + with open(_config_path, 'r', encoding='utf-8') as file: + origin_yaml = yaml.load(file.read(), Loader=yaml.FullLoader) for obj in origin_yaml: obj["src_db_file"] = "" with open(_config_path, 'w', encoding='utf-8') as w_f: yaml.dump(origin_yaml, w_f) + InitDataBase(config_file_path=_config_path).init_data() - with open(system_config.DATABASE_FILE_INFO, 'r', encoding='utf-8') as file_context: - init_database_date = yaml.load( - file_context.read(), Loader=yaml.FullLoader) + except TypeError as error: + self.assertEqual( - init_database_date, - None, - msg=" Priority must be a positive integer between 0 and 100 ") - except Exception as e: - return + error.__class__, + TypeError, + msg="Yaml file exists but DB_ Name duplicate exception type is wrong") finally: # Restore files os.remove(_config_path) os.rename(_config_path + '.bak', _config_path) def test_priority(self): + """test priority""" try: _config_path = ReadConfig().get_system('init_conf_path') shutil.copyfile(_config_path, _config_path + '.bak') - with open(_config_path, 'r', encoding='utf-8') as f: - origin_yaml = yaml.load(f.read(), Loader=yaml.FullLoader) + with open(_config_path, 'r', encoding='utf-8') as file: + origin_yaml = yaml.load(file.read(), Loader=yaml.FullLoader) for obj in origin_yaml: obj["priority"] = "-1" with open(_config_path, 'w', encoding='utf-8') as w_f: @@ -224,7 +225,7 @@ class ImportData(unittest.TestCase): init_database_date, None, msg=" Priority must be a positive integer between 0 and 100 ") - except Exception as e: + except FileNotFoundError: return finally: # Restore files @@ -232,9 +233,9 @@ class ImportData(unittest.TestCase): os.rename(_config_path + '.bak', _config_path) def test_true_init_data(self): - ''' + """ Initialization of system data - ''' + """ # Normal configuration _config_path = ReadConfig().get_system('init_conf_path') InitDataBase(config_file_path=_config_path).init_data() @@ -250,11 +251,11 @@ class ImportData(unittest.TestCase): # And there is data in this file, and it comes from the yaml file of # conf - with open(_config_path, 'r', encoding='utf-8') as f: - yaml_config = yaml.load(f.read(), Loader=yaml.FullLoader) + with open(_config_path, 'r', encoding='utf-8') as file: + yaml_config = yaml.load(file.read(), Loader=yaml.FullLoader) - with open(path, 'r', encoding='utf-8') as f: - yaml_success = yaml.load(f.read(), Loader=yaml.FullLoader) + with open(path, 'r', encoding='utf-8') as files: + yaml_success = yaml.load(files.read(), Loader=yaml.FullLoader) self.assertEqual( len(yaml_config), @@ -264,7 +265,8 @@ class ImportData(unittest.TestCase): # Compare name and priority success_name_priority = dict() config_name_priority = dict() - for i in range(len(yaml_config)): + len_con = len(yaml_config) + for i in range(len_con): success_name_priority[yaml_success[i]["database_name"]] = \ yaml_success[i]["priority"] config_name_priority[yaml_config[i]["dbname"]] = \ @@ -273,17 +275,5 @@ class ImportData(unittest.TestCase): self.assertEqual( success_name_priority, config_name_priority, - msg="The database and priority after initialization are inconsistent with the original file") - - -def test_import_data_suit(): - """test_import_data_suit""" - print("-----ImportData START----") - suite = unittest.TestSuite() - suite.addTest(ImportData("test_empty_param")) - suite.addTest(ImportData("test_wrong_param")) - suite.addTest(ImportData("test_dbname")) - suite.addTest(ImportData("test_src_db_file")) - suite.addTest(ImportData("test_priority")) - suite.addTest(ImportData("test_true_init_data")) - unittest.TextTestRunner().run(suite) + msg="The database and priority after initialization are" + "inconsistent with the original file") diff --git a/packageship/test/test_module/packages_tests/test_packages.py b/packageship/test/test_module/packages_tests/test_packages.py index 3c459bed..c549e0b6 100644 --- a/packageship/test/test_module/packages_tests/test_packages.py +++ b/packageship/test/test_module/packages_tests/test_packages.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ packges test @@ -14,7 +15,7 @@ class TestPackages(ReadTestBase): All package test cases """ - def test_empty_dbName(self): + def test_empty_dbname(self): """dbName is none""" resp = self.client.get("/packages") @@ -53,7 +54,7 @@ class TestPackages(ReadTestBase): resp_dict.get("data"), msg="Error in data information return") - def test_wrong_dbName(self): + def test_wrong_dbname(self): """dbName is err""" resp = self.client.get("/packages?dbName=test") @@ -77,13 +78,5 @@ class TestPackages(ReadTestBase): msg="Error in data information return") -def test_packages_suit(): - print("---TestPackages START---") - suite = unittest.TestSuite() - suite.addTest(TestPackages("test_empty_dbName")) - suite.addTest(TestPackages("test_wrong_dbName")) - unittest.TextTestRunner().run(suite) - - if __name__ == '__main__': unittest.main() diff --git a/packageship/test/test_module/repodatas_test/test_delete_repodatas.py b/packageship/test/test_module/repodatas_test/test_delete_repodatas.py index 1cb67ba7..dc396272 100644 --- a/packageship/test/test_module/repodatas_test/test_delete_repodatas.py +++ b/packageship/test/test_module/repodatas_test/test_delete_repodatas.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ test delete repodatas @@ -9,7 +10,7 @@ from test.base_code.operate_data_base import OperateTestBase import unittest import json from packageship import system_config -# from test.base_code.operate_data_base import system_config +from packageship.libs.exception import Error from packageship.application.apps.package.function.constants import ResponseCode @@ -89,20 +90,10 @@ class TestDeleteRepodatas(OperateTestBase): self.assertIsNone( resp_dict.get("data"), msg="Error in data information return") - except Exception as e: + except Error: return None finally: os.remove(system_config.DATABASE_FILE_INFO) os.rename(system_config.DATABASE_FILE_INFO + '.bak', system_config.DATABASE_FILE_INFO) shutil.rmtree(system_config.DATABASE_FOLDER_PATH) os.rename(system_config.DATABASE_FOLDER_PATH + '.bak', system_config.DATABASE_FOLDER_PATH) - - - -def test_delete_repodatas_suit(): - """unit testing""" - print("---TestDeleteRepodatas START---") - suite = unittest.TestSuite() - suite.addTest(TestDeleteRepodatas("test_wrong_dbname")) - suite.addTest(TestDeleteRepodatas("test_true_dbname")) - unittest.TextTestRunner().run(suite) diff --git a/packageship/test/test_module/repodatas_test/test_get_repodatas.py b/packageship/test/test_module/repodatas_test/test_get_repodatas.py index 581437f2..82dc3adb 100644 --- a/packageship/test/test_module/repodatas_test/test_get_repodatas.py +++ b/packageship/test/test_module/repodatas_test/test_get_repodatas.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ test get repodatas @@ -61,13 +62,5 @@ class TestGetRepodatas(ReadTestBase): msg="Error in data information return") -def test_get_repodatas_suit(): - """unit testing""" - print("---TestGetRepodatas START---") - suite = unittest.TestSuite() - suite.addTest(TestGetRepodatas("test_dbname")) - unittest.TextTestRunner().run(suite) - - if __name__ == '__main__': unittest.main() diff --git a/packageship/test/test_module/single_package_tests/test_get_singlepack.py b/packageship/test/test_module/single_package_tests/test_get_singlepack.py index 5d2addfc..d4702987 100644 --- a/packageship/test/test_module/single_package_tests/test_get_singlepack.py +++ b/packageship/test/test_module/single_package_tests/test_get_singlepack.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 # -*- coding:utf-8 -*- """ test_get_single_packages @@ -8,7 +9,6 @@ from test.base_code.read_data_base import ReadTestBase import unittest import json - from packageship.application.apps.package.function.constants import ResponseCode @@ -16,6 +16,7 @@ class TestGetSinglePack(ReadTestBase): """ Single package test case """ + def test_error_sourcename(self): """sourceName is none or err""" @@ -148,14 +149,6 @@ class TestGetSinglePack(ReadTestBase): resp_dict.get("data"), msg="Error in data information return") -def test_get_single_package_suit(): - """unit testing""" - print("---TestSinglePack START---") - suite = unittest.TestSuite() - suite.addTest(TestGetSinglePack("test_error_sourcename")) - suite.addTest(TestGetSinglePack("test_true_dbname")) - suite.addTest(TestGetSinglePack("test_wrong_dbname")) - unittest.TextTestRunner().run(suite) if __name__ == '__main__': unittest.main() diff --git a/packageship/test/test_module/single_package_tests/test_update_singlepack.py b/packageship/test/test_module/single_package_tests/test_update_singlepack.py index 6e21b77b..2b99c59a 100644 --- a/packageship/test/test_module/single_package_tests/test_update_singlepack.py +++ b/packageship/test/test_module/single_package_tests/test_update_singlepack.py @@ -1,15 +1,16 @@ +#!/usr/bin/python3 """TestUpdatePackage""" # -*- coding:utf-8 -*- from test.base_code.operate_data_base import OperateTestBase import unittest import json - from packageship.application.apps.package.function.constants import ResponseCode class TestUpdatePackage(OperateTestBase): """TestUpdatePackage""" + def test_empty_dbname(self): """Parameter error""" @@ -148,15 +149,6 @@ class TestUpdatePackage(OperateTestBase): resp_dict.get("data"), msg="Error in data information return") -def test_updata_single_package_suit(): - """unit testing""" - print("---TestUpdatePackage START---") - suite = unittest.TestSuite() - suite.addTest(TestUpdatePackage("test_empty_dbname")) - suite.addTest(TestUpdatePackage("test_empty_sourcename")) - suite.addTest(TestUpdatePackage("test_true_parram")) - unittest.TextTestRunner().run(suite) - if __name__ == '__main__': unittest.main() -- Gitee From d140099a0880c80b92f2ff99370c12b289f58d02 Mon Sep 17 00:00:00 2001 From: gongzt Date: Thu, 9 Jul 2020 10:31:32 +0800 Subject: [PATCH 69/72] Improvement of code defects --- .../packageship/application/initsystem/data_import.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packageship/packageship/application/initsystem/data_import.py b/packageship/packageship/application/initsystem/data_import.py index 1cbdfbc9..66c9e886 100644 --- a/packageship/packageship/application/initsystem/data_import.py +++ b/packageship/packageship/application/initsystem/data_import.py @@ -60,6 +60,15 @@ class InitDataBase(): if self.db_type not in ['mysql', 'sqlite']: LOGGER.logger.error("database type configuration error") raise Error('database type configuration error') + self._src_requires_dicts = dict() + self._bin_package_dicts = dict() + self._bin_package_name = dict() + self._bin_requires_dicts = dict() + self._bin_provides_dicts = dict() + self._src_packages = dict() + self._src_package_names = dict() + self._sqlite_db = None + self.requires = dict() def __read_config_file(self): """ -- Gitee From cef2e2521a6f0e8cfda0ecb69faa5d5827dcbb60 Mon Sep 17 00:00:00 2001 From: gongzt Date: Thu, 9 Jul 2020 10:32:23 +0800 Subject: [PATCH 70/72] Improvement of code defects --- .../packageship/application/initsystem/datamerge.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packageship/packageship/application/initsystem/datamerge.py b/packageship/packageship/application/initsystem/datamerge.py index 3123d1a3..85a6be7e 100644 --- a/packageship/packageship/application/initsystem/datamerge.py +++ b/packageship/packageship/application/initsystem/datamerge.py @@ -35,11 +35,12 @@ class MergeData(): self.db_file = db_file self.db_type = 'sqlite:///' self.datum_database = 'maintenance.information' - self.src_requires_dicts = None - self.src_package_datas = None - self.bin_provides_dicts = None - self.bin_package_datas = None - self.mainter_infos = None + self.src_requires_dicts = dict() + self.src_package_datas = [] + self.bin_provides_dicts = dict() + self.bin_package_datas = [] + self.mainter_infos = dict() + self.bin_requires_dicts = dict() @staticmethod def __columns(cursor): -- Gitee From a14314a4c6e492dc5553ce73f58b754c5d1d5b5d Mon Sep 17 00:00:00 2001 From: gongzt Date: Thu, 9 Jul 2020 10:50:45 +0800 Subject: [PATCH 71/72] File specification modification, including __init__ file description, single quote modification --- .../packageship/application/__init__.py | 8 ++++---- .../packageship/application/apps/__init__.py | 4 ++++ .../packageship/application/models/package.py | 16 ++++++--------- packageship/packageship/libs/__init__.py | 4 ++++ .../packageship/libs/dbutils/__init__.py | 6 +++++- .../packageship/libs/exception/__init__.py | 4 ++++ packageship/packageship/libs/log/__init__.py | 4 ++++ packageship/packageship/libs/log/loghelper.py | 20 +++++++++---------- packageship/setup.py | 5 +++++ 9 files changed, 46 insertions(+), 25 deletions(-) diff --git a/packageship/packageship/application/__init__.py b/packageship/packageship/application/__init__.py index 23cc8137..bc3a6316 100644 --- a/packageship/packageship/application/__init__.py +++ b/packageship/packageship/application/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -''' +""" Initial operation and configuration of the flask project -''' +""" from flask import Flask from flask_session import Session from packageship.application.settings import Config @@ -11,9 +11,9 @@ OPERATION = None def init_app(operation): - ''' + """ Project initialization function - ''' + """ app = Flask(__name__) # log configuration diff --git a/packageship/packageship/application/apps/__init__.py b/packageship/packageship/application/apps/__init__.py index 7734e498..6a86c78c 100644 --- a/packageship/packageship/application/apps/__init__.py +++ b/packageship/packageship/application/apps/__init__.py @@ -1,3 +1,7 @@ +#!/usr/bin/python3 +""" +Blueprint collection trying to page +""" from packageship.application.apps.package import package, api as package_api blue_point = [ diff --git a/packageship/packageship/application/models/package.py b/packageship/packageship/application/models/package.py index 62f7ec18..c0f8acb3 100644 --- a/packageship/packageship/application/models/package.py +++ b/packageship/packageship/application/models/package.py @@ -10,7 +10,6 @@ from packageship.libs.dbutils.sqlalchemy_helper import DBHelper class src_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 """ Description: functional description:Source package model - modify record: """ __tablename__ = 'src_pack' @@ -35,7 +34,6 @@ class src_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 class bin_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 """ Description: functional description:Binary package data - modify record: """ __tablename__ = 'bin_pack' @@ -51,10 +49,9 @@ class bin_pack(DBHelper.BASE): # pylint: disable=C0103,R0903 class pack_requires(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' + """ functional description: - modify record: - ''' + """ __tablename__ = 'pack_requires' @@ -72,10 +69,9 @@ class pack_requires(DBHelper.BASE): # pylint: disable=C0103,R0903 class pack_provides(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' + """ functional description: - modify record: - ''' + """ __tablename__ = 'pack_provides' id = Column(Integer, primary_key=True) @@ -86,9 +82,9 @@ class pack_provides(DBHelper.BASE): # pylint: disable=C0103,R0903 class maintenance_info(DBHelper.BASE): # pylint: disable=C0103,R0903 - ''' + """ Maintain data related to person information - ''' + """ __tablename__ = 'maintenance_info' id = Column(Integer, primary_key=True) diff --git a/packageship/packageship/libs/__init__.py b/packageship/packageship/libs/__init__.py index e69de29b..f4f5866b 100644 --- a/packageship/packageship/libs/__init__.py +++ b/packageship/packageship/libs/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 +""" +Encapsulation of public class methods +""" diff --git a/packageship/packageship/libs/dbutils/__init__.py b/packageship/packageship/libs/dbutils/__init__.py index c70f6e8f..78ac1617 100644 --- a/packageship/packageship/libs/dbutils/__init__.py +++ b/packageship/packageship/libs/dbutils/__init__.py @@ -1,3 +1,7 @@ +#!/usr/bin/python3 +""" +Database access public class method +""" from .sqlalchemy_helper import DBHelper -__all__ = ['DBHelper'] \ No newline at end of file +__all__ = ['DBHelper'] diff --git a/packageship/packageship/libs/exception/__init__.py b/packageship/packageship/libs/exception/__init__.py index 5f13a795..38fdb8dd 100644 --- a/packageship/packageship/libs/exception/__init__.py +++ b/packageship/packageship/libs/exception/__init__.py @@ -1,3 +1,7 @@ +#!/usr/bin/python3 +""" +Customized exception information class +""" from packageship.libs.exception.ext import ContentNoneException from packageship.libs.exception.ext import DatabaseRepeatException from packageship.libs.exception.ext import DataMergeException diff --git a/packageship/packageship/libs/log/__init__.py b/packageship/packageship/libs/log/__init__.py index 7b5fbf4d..3decd458 100644 --- a/packageship/packageship/libs/log/__init__.py +++ b/packageship/packageship/libs/log/__init__.py @@ -1,3 +1,7 @@ +#!/usr/bin/python3 +""" +Common methods for logging +""" from packageship.libs.log.loghelper import setup_log from packageship.libs.log.loghelper import Log diff --git a/packageship/packageship/libs/log/loghelper.py b/packageship/packageship/libs/log/loghelper.py index 91541477..190e43a7 100644 --- a/packageship/packageship/libs/log/loghelper.py +++ b/packageship/packageship/libs/log/loghelper.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 -''' +""" Logging related -''' +""" import os import pathlib import logging @@ -13,9 +13,9 @@ READCONFIG = ReadConfig() def setup_log(config=None): - ''' + """ Log logging in the context of flask - ''' + """ if config: logging.basicConfig(level=config.LOG_LEVEL) else: @@ -47,9 +47,9 @@ def setup_log(config=None): class Log(): - ''' + """ General log operations - ''' + """ def __init__(self, name=__name__, path=None): self.__name = name @@ -95,17 +95,17 @@ class Log(): self.__file_handler.setFormatter(formatter) def close_handler(self): - ''' + """ Turn off log processing - ''' + """ # self.__stream_handler.close() self.__file_handler.close() @property def logger(self): - ''' + """ Get logs - ''' + """ self.__ini_handler() self.__set_handler() self.__set_formatter() diff --git a/packageship/setup.py b/packageship/setup.py index 8059d79d..676e6ccb 100644 --- a/packageship/setup.py +++ b/packageship/setup.py @@ -1,3 +1,8 @@ +#!/usr/bin/python3 +""" +Package management program installation configuration +file for software packaging +""" from distutils.core import setup import os -- Gitee From 3473e18d080fca21f2a8131acc2dfc3ac5ecad39 Mon Sep 17 00:00:00 2001 From: gongzt Date: Thu, 9 Jul 2020 18:50:24 +0800 Subject: [PATCH 72/72] Description of __init__ file --- packageship/packageship/application/models/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packageship/packageship/application/models/__init__.py b/packageship/packageship/application/models/__init__.py index e69de29b..79752094 100644 --- a/packageship/packageship/application/models/__init__.py +++ b/packageship/packageship/application/models/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/python3 +""" +Entity mapping model of database +""" -- Gitee