From a76209b90017255cc0b5a46ea4e0db8aba1746f1 Mon Sep 17 00:00:00 2001 From: Yiru Wang Mac Date: Wed, 3 Mar 2021 21:43:32 +0800 Subject: [PATCH] update pkgship to 2.1.0 --- ...-installation-dependency-query-error.patch | 1632 --------- ...fix-the-problem-of-continuous-spaces.patch | 255 -- ...og_level-configuration-item-not-work.patch | 55 - ...-error-when-executing-query-commands.patch | 24 - ...n-source-package-has-no-sub-packages.patch | 62 - ...ice-crash-and-data-duplication-issue.patch | 3055 ----------------- ...d-change-the-status-recording-method.patch | 211 -- 0008-fix-selfbuild-error-message.patch | 12 - ...records-when-obtaining-issue-content.patch | 134 - pkgship-1.1.0.tar.gz | Bin 7555620 -> 0 bytes pkgship-2.1.0.tar.gz | Bin 0 -> 27304120 bytes pkgship.spec | 280 +- 12 files changed, 224 insertions(+), 5496 deletions(-) delete mode 100644 0001-solve-installation-dependency-query-error.patch delete mode 100644 0002-fix-the-problem-of-continuous-spaces.patch delete mode 100644 0003-fix-log_level-configuration-item-not-work.patch delete mode 100644 0004-fix-the-error-when-executing-query-commands.patch delete mode 100644 0005-fix-the-error-when-source-package-has-no-sub-packages.patch delete mode 100644 0006-fix-memory_caused-service-crash-and-data-duplication-issue.patch delete mode 100644 0007-correct-the-parameter-transfer-method-and-change-the-status-recording-method.patch delete mode 100644 0008-fix-selfbuild-error-message.patch delete mode 100644 0009-optimize-log-records-when-obtaining-issue-content.patch delete mode 100644 pkgship-1.1.0.tar.gz create mode 100644 pkgship-2.1.0.tar.gz diff --git a/0001-solve-installation-dependency-query-error.patch b/0001-solve-installation-dependency-query-error.patch deleted file mode 100644 index 42851bf..0000000 --- a/0001-solve-installation-dependency-query-error.patch +++ /dev/null @@ -1,1632 +0,0 @@ -diff --git a/packageship/application/apps/package/function/build_depend.py b/packageship/application/apps/package/function/build_depend.py -index 92351e7..b68eb91 100644 ---- a/packageship/application/apps/package/function/build_depend.py -+++ b/packageship/application/apps/package/function/build_depend.py -@@ -20,8 +20,10 @@ class BuildDepend(): - 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 - not_found_components: Contain the package not found components -+ __already_pk_val:List of pkgKey found - """ - -+ # pylint: disable = R0902 - def __init__(self, pkg_name_list, db_list, self_build=0, history_dict=None): - """ - init class -@@ -38,6 +40,8 @@ class BuildDepend(): - self.history_dicts = history_dict if history_dict else {} - self.not_found_components = set() - -+ self.__already_pk_val = [] -+ - def build_depend_main(self): - """ - Description: Entry function -@@ -67,7 +71,8 @@ class BuildDepend(): - # 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, self.not_found_components -+ return (ResponseCode.SUCCESS, self.result_dict, -+ self.source_dict, self.not_found_components) - - return ResponseCode.PARAM_ERROR, None, None, set() - -@@ -80,7 +85,13 @@ class BuildDepend(): - ResponseCode: response code - Raises: - """ -- res_status, build_list, not_fd_com_build = self.search_db.get_build_depend(pkg_list) -+ (res_status, -+ build_list, -+ not_fd_com_build, -+ pk_v -+ ) = self.search_db.get_build_depend(pkg_list, self.__already_pk_val) -+ -+ self.__already_pk_val += pk_v - self.not_found_components.update(not_fd_com_build) - if not build_list: - return res_status if res_status == ResponseCode.DIS_CONNECTION_DB else \ -@@ -91,7 +102,8 @@ class BuildDepend(): - - code, res_dict, not_fd_com_install = \ - InstallDepend(self.db_list).query_install_depend(search_list, -- self.history_dicts) -+ self.history_dicts, -+ self.__already_pk_val) - self.not_found_components.update(not_fd_com_install) - if not res_dict: - return code -@@ -189,7 +201,13 @@ class BuildDepend(): - return - - next_src_set = set() -- _, bin_info_lis, not_fd_com = self.search_db.get_build_depend(pkg_name_li) -+ (_, -+ bin_info_lis, -+ not_fd_com, -+ pk_v -+ ) = self.search_db.get_build_depend(pkg_name_li, -+ self.__already_pk_val) -+ self.__already_pk_val += pk_v - self.not_found_components.update(not_fd_com) - if not bin_info_lis: - return -diff --git a/packageship/application/apps/package/function/install_depend.py b/packageship/application/apps/package/function/install_depend.py -index f3cf05e..c4afe2e 100644 ---- a/packageship/application/apps/package/function/install_depend.py -+++ b/packageship/application/apps/package/function/install_depend.py -@@ -5,9 +5,8 @@ Description: Querying for install dependencies - class: InstallDepend, DictionaryOperations - """ - from packageship.libs.log import Log --from .searchdb import SearchDB --from .constants import ResponseCode --from .constants import ListNode -+from packageship.application.apps.package.function.searchdb import SearchDB -+from packageship.application.apps.package.function.constants import ResponseCode, ListNode - - LOGGER = Log(__name__) - -@@ -21,9 +20,11 @@ class InstallDepend(): - binary_dict: Contain all the binary packages info and operation - __search_db: A object of database which would be connected - not_found_components: Contain the package not found components -+ __already_pk_value: List of pkgKey found - changeLog: - """ -- #pylint: disable = too-few-public-methods -+ -+ # pylint: disable = too-few-public-methods - def __init__(self, db_list): - """ - Initialization class -@@ -34,14 +35,16 @@ class InstallDepend(): - self.db_list = db_list - self.__search_db = SearchDB(db_list) - self.not_found_components = set() -+ self.__already_pk_value = [] - -- def query_install_depend(self, binary_list, history_dicts=None): -+ def query_install_depend(self, binary_list, history_pk_val=None, history_dicts=None): - """ - Description: init result dict and determint the loop end point - Args: - binary_list: A list of binary rpm package name - history_dicts: record the searching install depend history, - defualt is None -+ history_pk_val:List of pkgKey found - Returns: - binary_dict.dictionary: - {binary_name: [ -@@ -64,7 +67,8 @@ class InstallDepend(): - if binary: - self.__search_list.append(binary) - else: -- LOGGER.logger.warning("There is a NONE in input value:" + str(binary_list)) -+ LOGGER.logger.warning("There is a NONE in input value: %s", str(binary_list)) -+ self.__already_pk_value += history_pk_val if history_pk_val else [] - while self.__search_list: - self.__query_single_install_dep(history_dicts) - return ResponseCode.SUCCESS, self.binary_dict.dictionary, self.not_found_components -@@ -78,8 +82,14 @@ class InstallDepend(): - response_code: response code - Raises: - """ -- result_list, not_found_components = map(set, self.__search_db.get_install_depend(self.__search_list)) -+ result_list, not_found_components, pk_val = map( -+ set, -+ self.__search_db.get_install_depend(self.__search_list, -+ self.__already_pk_value) -+ ) -+ - self.not_found_components.update(not_found_components) -+ self.__already_pk_value += pk_val - for search in self.__search_list: - if search not in self.binary_dict.dictionary: - self.binary_dict.init_key(key=search, parent_node=[]) -@@ -108,7 +118,7 @@ class InstallDepend(): - 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']]) -@@ -129,6 +139,7 @@ class DictionaryOperations(): - """ - self.dictionary = dict() - -+ # pylint: disable=R0913 - def init_key(self, key, src=None, version=None, dbname=None, parent_node=None): - """ - Description: Creating dictionary -@@ -146,6 +157,7 @@ class DictionaryOperations(): - else: - self.dictionary[key] = [src, version, dbname, parent_node] - -+ # pylint: disable=R0913 - def update_value(self, key, src=None, version=None, dbname=None, parent_node=None): - """ - Description: append dictionary -diff --git a/packageship/application/apps/package/function/searchdb.py b/packageship/application/apps/package/function/searchdb.py -index 400d422..1624e0d 100644 ---- a/packageship/application/apps/package/function/searchdb.py -+++ b/packageship/application/apps/package/function/searchdb.py -@@ -4,7 +4,7 @@ Description: A set for all query databases function - class: SearchDB - functions: db_priority - """ --from collections import namedtuple -+from collections import namedtuple, Counter - - import yaml - from flask import current_app -@@ -15,10 +15,10 @@ from sqlalchemy import exists - - from packageship.libs.dbutils import DBHelper - from packageship.libs.log import Log --from packageship.application.models.package import BinPack,SrcPack -+from packageship.application.models.package import BinPack, SrcPack - from packageship.libs.exception import ContentNoneException, Error - from packageship.system_config import DATABASE_FILE_INFO --from .constants import ResponseCode -+from packageship.application.apps.package.function.constants import ResponseCode - - LOGGER = Log(__name__) - -@@ -50,343 +50,231 @@ class SearchDB(): - except DisconnectionError as connection_error: - current_app.logger.error(connection_error) - -- def get_install_depend(self, binary_list): -+ # Related methods of install -+ # pylint: disable=R0914 -+ def get_install_depend(self, binary_list, pk_value=None): - """ - 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 - Args: - binary_list: a list of binary package name -+ pk_value:List of pkgKey found - Returns: - list:install depend list -- set:package not found components -+ set:package not found components, -+ pk_val:The pkgkey corresponding to the required components - Raises: - """ -+ pk_val = pk_value if pk_value else [] - result_list = [] -- get_list = [] - provides_not_found = dict() -+ - if not self.db_object_dict: -- LOGGER.logger.warning("Unable to connect to the database, \ -- check the database configuration") -- return result_list -+ LOGGER.logger.warning("Unable to connect to the database," -+ "check the database configuration") -+ return result_list, set(), pk_val -+ - if None in binary_list: - 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.") -- return result_list -- return_tuple = namedtuple('return_tuple', -- 'depend_name depend_version depend_src_name \ -- search_name search_src_name search_version') -+ LOGGER.logger.warning("The input is None, please check the input value.") -+ return result_list, set(), pk_val -+ -+ return_tuple = namedtuple('return_tuple', [ -+ 'depend_name', -+ 'depend_version', -+ 'depend_src_name', -+ 'search_name', -+ 'search_src_name', -+ 'search_version' -+ ]) -+ - 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_requires.NAME AS req_name, -- bin.NAME AS search_name, -- s1.name AS search_src_name, -- bin.version AS search_version -- FROM -- ( SELECT pkgKey, NAME, version, rpm_sourcerpm FROM bin_pack WHERE {} ) bin -- LEFT JOIN src_pack s1 ON bin.rpm_sourcerpm = s1.src_name -- LEFT JOIN bin_requires ON bin.pkgKey = bin_requires.pkgKey -- LEFT JOIN bin_provides ON bin_provides.name = bin_requires.name -- LEFT JOIN bin_pack ON bin_pack.pkgKey = bin_provides.pkgKey -- LEFT JOIN src_pack s2 ON bin_pack.rpm_sourcerpm = s2.src_name; -- """.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: -- get_list.append(result.search_name) -- if not result.depend_name and result.req_name: -- if result.req_name in provides_not_found: -- provides_not_found[result.req_name].append( -- [result.search_name, result.search_src_name, result.search_version, db_name]) -- else: -- provides_not_found[result.req_name] = [ -- [result.search_name, result.search_src_name, result.search_version, db_name]] -- else: -- obj = return_tuple( -- result.depend_name, -- result.depend_src_name, -- result.depend_version, -- result.search_name, -- result.search_src_name, -- result.search_version, -- ) -- result_list.append((obj, db_name)) -- get_set = set(get_list) -- get_list.clear() -- search_set.symmetric_difference_update(get_set) -- if not search_set: -- install_result = self._get_install_pro_in_other_database( -- provides_not_found) -- result_list.extend(install_result) -- return result_list, set(provides_not_found.keys()) -- else: -+ req_set = self._get_requires(search_set, data_base, _tp='install') -+ -+ if not req_set: - continue -- except AttributeError as error_msg: -- LOGGER.logger.error(error_msg) -- except SQLAlchemyError as error_msg: -- LOGGER.logger.error(error_msg) -- install_result = self._get_install_pro_in_other_database( -- provides_not_found) -- result_list.extend(install_result) -- for binary_name in search_set: -- result_list.append((return_tuple(None, None, None, -- binary_name, None, None), 'NOT FOUND')) -- return result_list, set(provides_not_found.keys()) - -- def get_src_name(self, binary_name): -- """ -- Description: get a package source name from database: -- bianry_name ->binary_source_name -> source_name -- 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(): -- sql_str = """ -- SELECT DISTINCT -- src_pack.name AS source_name, -- src_pack.version AS source_version -- FROM -- bin_pack, -- src_pack -- WHERE -- src_pack.src_name = bin_pack.rpm_sourcerpm -- AND bin_pack.name = :binary_name; -- """ -- try: -- bin_obj = data_base.session.execute(text(sql_str), {"binary_name": binary_name}).fetchone() -- source_name = bin_obj.source_name -- source_version = bin_obj.source_version -- if source_name is not None: -- return ResponseCode.SUCCESS, db_name, \ -- source_name, source_version -+ (depend_set, -+ req_pk_dict, -+ pk_v, -+ not_fd_com) = self._get_provides_req_info(req_set, -+ data_base, -+ pk_val) -+ pk_val += pk_v -+ res_list, get_list = self._comb_install_list(depend_set, -+ req_pk_dict, -+ not_fd_com, -+ return_tuple, -+ db_name, -+ provides_not_found, -+ req_set) -+ -+ result_list += res_list -+ -+ search_set.symmetric_difference_update(set(get_list)) -+ -+ if not search_set: -+ result_list.extend( -+ self._get_install_pro_in_other_database(provides_not_found, -+ db_name) -+ ) -+ return result_list, set(provides_not_found.keys()), pk_val -+ - 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, None, None -- return ResponseCode.PACK_NAME_NOT_FOUND, None, None, None -- -- def get_sub_pack(self, source_name_list): -+ if search_set: -+ result_list.extend( -+ self._get_install_pro_in_other_database(provides_not_found) -+ ) -+ -+ for binary_name in search_set: -+ result_list.append((return_tuple(None, None, None, -+ binary_name, None, None), 'NOT FOUND')) -+ return result_list, set(provides_not_found.keys()), pk_val -+ -+ # pylint: disable=R0913 -+ @staticmethod -+ def _comb_install_list(depend_set, -+ req_pk_dict, -+ not_fd_com, -+ return_tuple, -+ db_name, -+ provides_not_found, -+ req_set): - """ -- Description: get a subpack list based on source name list: -- source_name ->source_name_id -> binary_name -+ Description: Query the corresponding installation dependency list -+ through the components of the requirements - Args: -- source_name_list: search package's name, database preority list -+ depend_set: List binary package information corresponding to the components -+ req_pk_dict:Mapping of components and binary pkgKey -+ not_fd_com: List of pkgKey found, -+ return_tuple: Named tuple format for saving information -+ db_name:current database name -+ provides_not_found:Component mapping not found in the current database -+ req_set:Package information and corresponding component information - Returns: -- response code -- result_list: subpack tuple -+ ret_list:install depend list -+ get_list:Packages that have found results - Raises: -- AttributeError: The object does not have this property -- SQLAlchemyError: sqlalchemy error - """ -- if not self.db_object_dict: -- return ResponseCode.DIS_CONNECTION_DB, None -- search_set = set([ -- source_name for source_name in source_name_list if source_name]) -- 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 -- bin_pack.name AS subpack_name, -- bin_pack.version AS sub_pack_version, -- src.name AS search_name, -- src.version AS search_version -- FROM -- (SELECT name,version,src_name FROM src_pack WHERE {}) src -- LEFT JOIN bin_pack on src.src_name = bin_pack.rpm_sourcerpm'''.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 sub_pack_version search_version search_name') -- for search_name in search_set: -- result_list.append( -- (return_tuple(None, None, None, search_name), 'NOT_FOUND')) -- return ResponseCode.SUCCESS, result_list -+ ret_list = [] -+ depend_info_tuple = namedtuple('depend_info', [ -+ 'depend_name', -+ 'depend_version', -+ 'depend_src_name' -+ ]) -+ depend_info_dict = { -+ info.pk: depend_info_tuple(info.depend_name, -+ info.depend_version, -+ info.depend_src_name) -+ for info in depend_set -+ } -+ -+ for req_name, search_name, search_src_name, search_version in req_set: -+ get_list.append(search_name) -+ -+ if not req_name: -+ obj = return_tuple( -+ None, -+ None, -+ None, -+ search_name, -+ search_src_name, -+ search_version, -+ ) -+ ret_list.append((obj, db_name)) -+ -+ elif req_name in req_pk_dict: -+ depend_info_t = depend_info_dict.get(req_pk_dict[req_name]) -+ obj = return_tuple( -+ depend_info_t.depend_name, -+ depend_info_t.depend_version, -+ depend_info_t.depend_src_name, -+ search_name, -+ search_src_name, -+ search_version, -+ ) -+ ret_list.append((obj, db_name)) -+ -+ else: -+ if req_name in not_fd_com: -+ if req_name not in provides_not_found: -+ provides_not_found[req_name] = [[search_name, search_src_name, -+ search_version, db_name]] -+ else: -+ provides_not_found[req_name].append([search_name, search_src_name, -+ search_version, db_name]) -+ -+ return ret_list, get_list - -- def _get_binary_in_other_database(self, not_found_binary): -+ def _get_install_pro_in_other_database(self, not_found_binary, _db_name=None): - """ - 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 -+ _db_name: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),] -+ result_list :[return_tuple1,return_tuple2] package information - Raises: -- AttributeError: The object does not have this property -- SQLAlchemyError: sqlalchemy error - """ - if not not_found_binary: - return [] - -- return_tuple = namedtuple("return_tuple", [ -- "search_name", -- "source_name", -- "bin_name", -- "version", -- "db_name", -- "search_version", -+ return_tuple = namedtuple('return_tuple', [ -+ 'depend_name', -+ 'depend_version', -+ 'depend_src_name', -+ 'search_name', -+ 'search_src_name', -+ 'search_version' - ]) -- search_list = [] -+ - result_list = [] -+ search_set = {k for k, _ in not_found_binary.items()} -+ - for db_name, data_base in self.db_object_dict.items(): -- for key, _ in not_found_binary.items(): -- search_list.append(key) -+ if db_name == _db_name: -+ continue - -- search_set = set(search_list) -- search_list.clear() -- try: -- sql_string = text(""" -- SELECT DISTINCT -- s1.name AS source_name, -- t1.NAME AS bin_name, -- t1.version, -- t2.NAME AS req_name -- FROM -- src_pack s1, -- bin_pack t1, -- bin_provides t2 -- WHERE -- t2.{} -- AND t1.pkgKey = t2.pkgKey -- AND t1.rpm_sourcerpm = s1.src_name; -- """.format(literal_column('name').in_(search_set))) -- bin_set = data_base.session. \ -- execute(sql_string, {'name_{}'.format(i): v -- for i, v in enumerate(search_set, 1)}).fetchall() -- if bin_set: -- for result in bin_set: -- if result.req_name not in not_found_binary: -- LOGGER.logger.warning( -- result.req_name + " contains in two rpm packages!!!") -- else: -- for source_info in not_found_binary[result.req_name]: -- obj = return_tuple( -- source_info[0], -- result.source_name, -- result.bin_name, -- result.version, -- db_name, -- source_info[1] -- ) -- result_list.append(obj) -- del not_found_binary[result.req_name] -- if not not_found_binary: -- return result_list -- except AttributeError as attr_err: -- current_app.logger.error(attr_err) -- except SQLAlchemyError as sql_err: -- current_app.logger.error(sql_err) -+ parm_tuple = namedtuple("in_tuple", 'req_name') -+ in_tuple_list = [parm_tuple(k) for k, _ in not_found_binary.items()] -+ -+ depend_set, req_pk_dict, *_ = self._get_provides_req_info( -+ in_tuple_list, -+ data_base -+ ) -+ -+ depend_info_tuple = namedtuple('depend_info', [ -+ 'depend_name', -+ 'depend_version', -+ 'depend_src_name' -+ ]) -+ depend_info_dict = { -+ info.pk: depend_info_tuple(info.depend_name, -+ info.depend_version, -+ info.depend_src_name) -+ for info in depend_set -+ } -+ result_list += self._comb_install_info(search_set, -+ req_pk_dict, -+ depend_info_dict, -+ not_found_binary, -+ return_tuple, -+ db_name) -+ if not not_found_binary: -+ return result_list - - if not_found_binary: -- for key, values in not_found_binary.items(): -- for info in values: -- obj = return_tuple( -- info[0], -- None, -- None, -- None, -- 'NOT FOUND', -- info[2] -- ) -- result_list.append(obj) -- return result_list -- -- def _get_install_pro_in_other_database(self, not_found_binary): -- if not not_found_binary: -- return [] -- return_tuple = namedtuple('return_tuple', -- 'depend_name depend_version depend_src_name \ -- search_name search_src_name search_version') -- search_list = [] -- result_list = [] -- for db_name, data_base in self.db_object_dict.items(): -- for key, values in not_found_binary.items(): -- search_list.append(key) -- search_set = set(search_list) -- search_list.clear() -- sql_string = text(""" -- SELECT DISTINCT -- s1.name AS source_name, -- t1.NAME AS bin_name, -- t1.version, -- t2.NAME AS req_name -- FROM -- src_pack s1, -- bin_pack t1, -- bin_provides t2 -- WHERE -- t2.{} -- AND t1.pkgKey = t2.pkgKey -- AND t1.rpm_sourcerpm = s1.src_name; -- """.format(literal_column('name').in_(search_set))) -- bin_set = data_base.session. \ -- execute(sql_string, {'name_{}'.format(i): v -- for i, v in enumerate(search_set, 1)}).fetchall() -- if bin_set: -- for result in bin_set: -- if result.req_name not in not_found_binary: -- LOGGER.logger.warning( -- result.req_name + " contains in two rpm packages!!!") -- else: -- for binary_info in not_found_binary[result.req_name]: -- obj = return_tuple( -- result.bin_name, -- result.version, -- result.source_name, -- binary_info[0], -- binary_info[1], -- binary_info[2] -- ) -- result_list.append((obj, binary_info[3])) -- del not_found_binary[result.req_name] -- if not not_found_binary: -- return result_list -- if not_found_binary: -- for key, values in not_found_binary.items(): -+ for _, values in not_found_binary.items(): - for info in values: - obj = return_tuple( - None, -@@ -399,11 +287,52 @@ class SearchDB(): - result_list.append((obj, info[3])) - return result_list - -- def get_build_depend(self, source_name_li): -+ @staticmethod -+ def _comb_install_info(search_set, -+ req_pk_dict, -+ depend_info_dict, -+ not_found_binary, -+ return_tuple, -+ db_name): -+ """ -+ Description: Binary package name data not found in -+ the current database, go to other databases to try -+ Args: -+ search_set: The name of the component to be queried -+ req_pk_dict:Mapping of components and binary pkgKey -+ depend_info_dict:The mapping of binary pkgKey and binary information -+ not_found_binary:not_found_build These data cannot be found in the current database -+ return_tuple:Named tuple format for saving information -+ db_name:current database name -+ Returns: -+ ret_list :[return_tuple1,return_tuple2] package information -+ Raises: -+ """ -+ ret_list = [] -+ for req_name in search_set: -+ if req_name in req_pk_dict: -+ pk_ = req_pk_dict[req_name] -+ if pk_ in depend_info_dict: -+ for binary_info in not_found_binary[req_name]: -+ obj = return_tuple( -+ depend_info_dict[pk_].depend_name, -+ depend_info_dict[pk_].depend_version, -+ depend_info_dict[pk_].depend_src_name, -+ binary_info[0], -+ binary_info[1], -+ binary_info[2] -+ ) -+ ret_list.append((obj, db_name)) -+ del not_found_binary[req_name] -+ return ret_list -+ -+ # Related methods of build -+ def get_build_depend(self, source_name_li, pk_value=None): - """ - Description: get a package build depend from database - Args: - source_name_li: search package's name list -+ pk_value:List of pkgKey found - Returns: - all source pkg build depend list - structure :[(search_name,source_name,bin_name,bin_version,db_name,search_version), -@@ -422,93 +351,428 @@ class SearchDB(): - "db_name", - "search_version" - ]) -- -+ pk_val = pk_value if pk_value else [] - s_name_set = set(source_name_li) - if not s_name_set: -- return ResponseCode.PARAM_ERROR, set() -+ return ResponseCode.PARAM_ERROR, list(), set(), pk_val - - provides_not_found = dict() - build_list = [] - - for db_name, data_base in self.db_object_dict.items(): - -- build_set = [] - try: -- temp_list = list(s_name_set) -- for input_name_li in [temp_list[i:i + 900] for i in range(0, len(temp_list), 900)]: -- sql_com = text(""" -- SELECT DISTINCT -- src.NAME AS search_name, -- src.version AS search_version, -- s1.name AS source_name, -- bin_provides.pkgKey AS bin_id, -- src_requires.NAME AS req_name, -- bin_pack.version AS version, -- bin_pack.NAME AS bin_name -- FROM -- ( SELECT pkgKey, NAME, version FROM src_pack WHERE {}) src -- LEFT JOIN src_requires ON src.pkgKey = src_requires.pkgKey -- LEFT JOIN bin_provides ON bin_provides.NAME = src_requires.NAME -- LEFT JOIN bin_pack ON bin_pack.pkgKey = bin_provides.pkgKey -- LEFT JOIN src_pack s1 on bin_pack.rpm_sourcerpm=s1.src_name; -- """.format(literal_column("name").in_(input_name_li))) -- res = data_base.session.execute( -- sql_com, -- {'name_{}'.format(i): v -- for i, v in enumerate(input_name_li, 1)} -- ).fetchall() -- -- build_set.extend(res) -+ req_set = self._get_requires(s_name_set, data_base, _tp='build') -+ -+ if not req_set: -+ continue -+ -+ (depend_set, -+ req_pk_dict, -+ pk_v, -+ not_fd_req) = self._get_provides_req_info(req_set, data_base) -+ -+ pk_val += pk_v -+ ret_list, get_list = self._comb_build_list(depend_set, -+ req_pk_dict, -+ not_fd_req, -+ return_tuple, -+ db_name, -+ provides_not_found, -+ req_set) -+ build_list += ret_list -+ s_name_set.symmetric_difference_update(set(get_list)) -+ if not s_name_set: -+ build_list.extend( -+ self._get_binary_in_other_database(provides_not_found, _db_name=db_name) -+ ) -+ return ResponseCode.SUCCESS, build_list, set(provides_not_found.keys()), pk_val -+ - except AttributeError as attr_err: - current_app.logger.error(attr_err) - except SQLAlchemyError as sql_err: - current_app.logger.error(sql_err) - -- if not build_set: -+ if s_name_set: -+ build_list.extend( -+ self._get_binary_in_other_database(provides_not_found) -+ ) -+ for source in s_name_set: -+ LOGGER.logger.warning( -+ "CANNOT FOUND THE SOURCE %s in all database", source) -+ -+ return ResponseCode.SUCCESS, build_list, set(provides_not_found.keys()), pk_val -+ -+ @staticmethod -+ def _comb_build_list(depend_set, -+ req_pk_dict, -+ not_fd_com, -+ return_tuple, -+ db_name, -+ provides_not_found, -+ req_set): -+ """ -+ Description: Query the corresponding build dependency list -+ through the components of the requirements -+ Args: -+ depend_set: List binary package information corresponding to the components -+ req_pk_dict:Mapping of components and binary pkgKey -+ not_fd_com: List of pkgKey found, -+ return_tuple: Named tuple format for saving information -+ db_name:current database name -+ provides_not_found:Component mapping not found in the current database -+ req_set:Package information and corresponding component information -+ Returns: -+ ret_list:install depend list -+ get_list:Packages that have found results -+ Raises: -+ """ -+ get_list = [] -+ ret_list = [] -+ depend_info_tuple = namedtuple('depend_info', [ -+ 'depend_name', -+ 'depend_version', -+ 'depend_src_name' -+ ]) -+ depend_info_dict = { -+ info.pk: depend_info_tuple(info.depend_name, -+ info.depend_version, -+ info.depend_src_name) -+ for info in depend_set -+ } -+ -+ for req_name, search_name, search_version in req_set: -+ -+ get_list.append(search_name) -+ -+ if not req_name: -+ obj = return_tuple( -+ search_name, -+ None, -+ None, -+ None, -+ db_name, -+ search_version, -+ ) -+ ret_list.append(obj) -+ -+ elif req_name in req_pk_dict: -+ depend_info_t = depend_info_dict.get(req_pk_dict[req_name]) -+ obj = return_tuple( -+ search_name, -+ depend_info_t.depend_src_name, -+ depend_info_t.depend_name, -+ depend_info_t.depend_version, -+ db_name, -+ search_version -+ ) -+ ret_list.append(obj) -+ -+ else: -+ if req_name in not_fd_com: -+ if req_name not in provides_not_found: -+ provides_not_found[req_name] = [ -+ [search_name, -+ search_version, -+ db_name] -+ ] -+ else: -+ provides_not_found[req_name].append([search_name, -+ search_version, -+ db_name]) -+ -+ return ret_list, get_list -+ -+ def _get_binary_in_other_database(self, not_found_binary, _db_name=None): -+ """ -+ 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_name:current database name -+ Returns: -+ result_list :[return_tuple1,return_tuple2] package information -+ Raises: -+ AttributeError: The object does not have this property -+ SQLAlchemyError: sqlalchemy error -+ """ -+ if not not_found_binary: -+ return [] -+ -+ return_tuple = namedtuple("return_tuple", [ -+ "search_name", -+ "source_name", -+ "bin_name", -+ "version", -+ "db_name", -+ "search_version", -+ ]) -+ -+ result_list = [] -+ search_set = {k for k, _ in not_found_binary.items()} -+ -+ for db_name, data_base in self.db_object_dict.items(): -+ -+ if db_name == _db_name: - continue - -- # When processing source package without compilation dependency -- get_list = [] -- for result in build_set: -- get_list.append(result.search_name) -- if not result.bin_name and result.req_name: -- if result.req_name in provides_not_found: -- provides_not_found[result.req_name].append( -- [result.search_name, result.search_version, db_name] -- ) -- else: -- provides_not_found[result.req_name] = [ -- [result.search_name, result.search_version, db_name] -- ] -- else: -+ in_tuple = namedtuple("in_tuple", 'req_name') -+ in_tuple_list = [in_tuple(k) for k, _ in not_found_binary.items()] -+ -+ depend_set, req_pk_dict, *_ = self._get_provides_req_info( -+ in_tuple_list, -+ data_base -+ ) -+ -+ depend_info_tuple = namedtuple('depend_info', [ -+ 'depend_name', -+ 'depend_version', -+ 'depend_src_name' -+ ]) -+ depend_info_dict = { -+ info.pk: depend_info_tuple(info.depend_name, -+ info.depend_version, -+ info.depend_src_name) -+ for info in depend_set -+ } -+ -+ result_list += self._comb_build_info(search_set, -+ req_pk_dict, -+ depend_info_dict, -+ not_found_binary, -+ return_tuple, -+ db_name) -+ if not not_found_binary: -+ return result_list -+ -+ if not_found_binary: -+ for _, values in not_found_binary.items(): -+ for info in values: - obj = return_tuple( -- result.search_name, -- result.source_name, -- result.bin_name, -- result.version, -- db_name, -- result.search_version -+ info[0], -+ None, -+ None, -+ None, -+ 'NOT FOUND', -+ info[2] - ) -- build_list.append(obj) -+ result_list.append(obj) -+ return result_list - -- get_set = set(get_list) -- get_list.clear() -- s_name_set.symmetric_difference_update(get_set) -- if not s_name_set: -- build_result = self._get_binary_in_other_database( -- provides_not_found) -- build_list.extend(build_result) -- return ResponseCode.SUCCESS, build_list, set(provides_not_found.keys()) -+ @staticmethod -+ def _comb_build_info(search_set, -+ req_pk_dict, -+ depend_info_dict, -+ not_found_binary, -+ return_tuple, -+ db_name): -+ """ -+ Description: Binary package name data not found in -+ the current database, go to other databases to try -+ Args: -+ search_set: The name of the component to be queried -+ req_pk_dict:Mapping of components and binary pkgKey -+ depend_info_dict:The mapping of binary pkgKey and binary information -+ not_found_binary:not_found_build These data cannot be found in the current database -+ return_tuple:Named tuple format for saving information, -+ db_name:current data base name -+ Returns: -+ ret_list :[return_tuple1,return_tuple2] package information -+ Raises: -+ """ -+ ret_list = [] -+ for req_name in search_set: -+ if req_name in req_pk_dict: -+ pk_ = req_pk_dict[req_name] -+ if pk_ in depend_info_dict: -+ for binary_info in not_found_binary[req_name]: -+ obj = return_tuple( -+ binary_info[0], -+ depend_info_dict[pk_].depend_src_name, -+ depend_info_dict[pk_].depend_name, -+ depend_info_dict[pk_].depend_version, -+ db_name, -+ binary_info[1] -+ ) -+ ret_list.append(obj) -+ del not_found_binary[req_name] -+ return ret_list - -- if s_name_set: -- build_result = self._get_binary_in_other_database( -- provides_not_found) -- build_list.extend(build_result) -- for source in s_name_set: -- LOGGER.logger.warning( -- "CANNOT FOUND THE source " + source + " in all database") -- return ResponseCode.SUCCESS, build_list, set(provides_not_found.keys()) -+ # Common methods for install and build -+ @staticmethod -+ def _get_requires(search_set, data_base, _tp=None): -+ """ -+ Description: Query the dependent components of the current package -+ Args: -+ search_set: The package name to be queried -+ data_base:current database object -+ _tp:type options build or install -+ Returns: -+ req_set:List Package information and corresponding component information -+ Raises: -+ AttributeError: The object does not have this property -+ SQLAlchemyError: sqlalchemy error -+ """ -+ if _tp == 'build': -+ sql_com = text(""" -+ SELECT DISTINCT -+ src_requires.NAME AS req_name, -+ src.NAME AS search_name, -+ src.version AS search_version -+ FROM -+ ( SELECT pkgKey, NAME, version, src_name FROM src_pack WHERE {} ) src -+ LEFT JOIN src_requires ON src.pkgKey = src_requires.pkgKey; -+ """.format(literal_column('name').in_(search_set))) -+ elif _tp == 'install': -+ sql_com = text(""" -+ SELECT DISTINCT -+ bin_requires.NAME AS req_name, -+ bin.NAME AS search_name, -+ s1.name as search_src_name, -+ bin.version AS search_version -+ FROM -+ ( SELECT pkgKey, NAME, version, rpm_sourcerpm FROM bin_pack WHERE {} ) bin -+ LEFT JOIN src_pack s1 ON bin.rpm_sourcerpm = s1.src_name -+ LEFT JOIN bin_requires ON bin.pkgKey = bin_requires.pkgKey; -+ """.format(literal_column('name').in_(search_set))) -+ else: -+ return [] - -+ req_set = [] -+ try: -+ req_set = data_base.session. \ -+ execute(sql_com, {'name_{}'.format(i): v -+ for i, v in enumerate(search_set, 1)}).fetchall() -+ except AttributeError as error_msg: -+ LOGGER.logger.error(error_msg) -+ except SQLAlchemyError as error_msg: -+ LOGGER.logger.error(error_msg) -+ return req_set -+ -+ def _get_provides_req_info(self, req_info, data_base, pk_value=None): -+ """ -+ Description: Get the name of the binary package -+ that provides the dependent component, -+ Filter redundant queries -+ when the same binary package is provided to multiple components -+ Args: -+ req_info: List of sqlalchemy objects with component names. -+ data_base: The database currently being queried -+ pk_value:Binary pkgKey that has been found -+ Returns: -+ depend_set: List of related dependent sqlalchemy objects -+ req_pk_dict: Mapping dictionary of component name and pkgKey -+ pk_val:update Binary pkgKey that has been found -+ not_fd_req: Components not found -+ Raises: -+ AttributeError: The object does not have this property -+ SQLAlchemyError: sqlalchemy error -+ """ -+ pk_val = pk_value if pk_value else [] -+ depend_set = [] -+ req_pk_dict = {} -+ not_fd_req = set() -+ try: -+ req_names = {req_.req_name -+ for req_ in req_info -+ if req_.req_name is not None} -+ req_name_in = literal_column('name').in_(req_names) -+ -+ sql_com_pro = text(""" -+ SELECT DISTINCT -+ NAME as req_name, -+ pkgKey -+ FROM -+ ( SELECT name, pkgKey FROM bin_provides -+ UNION ALL -+ SELECT name, pkgKey FROM bin_files ) -+ WHERE -+ {}; -+ """.format(req_name_in)) -+ -+ pkg_key_set = data_base.session.execute( -+ sql_com_pro, { -+ 'name_{}'.format(i): v -+ for i, v in enumerate(req_names, 1) -+ } -+ ).fetchall() -+ -+ req_pk_dict = dict() -+ pk_v = list() -+ -+ for req_name, pk_ in pkg_key_set: -+ if not req_name: -+ continue -+ pk_v.append(pk_) -+ if req_name not in req_pk_dict: -+ req_pk_dict[req_name] = [pk_] -+ else: -+ req_pk_dict[req_name].append(pk_) -+ -+ pk_val += pk_v -+ -+ pk_count_dic = Counter(pk_val) -+ -+ for key, values in req_pk_dict.items(): -+ count_values = list(map( -+ lambda x: pk_count_dic[x] if x in pk_count_dic else 0, values -+ )) -+ max_index = count_values.index(max(count_values)) -+ req_pk_dict[key] = values[max_index] -+ -+ not_fd_req = req_names - set(req_pk_dict.keys()) -+ depend_set = self._get_depend_info(req_pk_dict, data_base) -+ -+ except SQLAlchemyError as sql_err: -+ LOGGER.logger.error(sql_err) -+ except AttributeError as error_msg: -+ LOGGER.logger.error(error_msg) -+ -+ return depend_set, req_pk_dict, pk_val, not_fd_req -+ -+ @staticmethod -+ def _get_depend_info(req_pk_dict, data_base): -+ """ -+ Description: Obtain binary related information through binary pkgKey -+ Args: -+ req_pk_dict: Mapping dictionary of component name and pkgKey -+ data_base: The database currently being queried -+ Returns: -+ depend_set: List of related dependent sqlalchemy objects -+ Raises: -+ AttributeError: The object does not have this property -+ SQLAlchemyError: sqlalchemy error -+ """ -+ depend_set = [] -+ try: -+ bin_src_pkg_key = req_pk_dict.values() -+ pk_in = literal_column('pkgKey').in_(bin_src_pkg_key) -+ sql_bin_src = text(""" -+ SELECT DISTINCT -+ bin.pkgKey as pk, -+ bin.name AS depend_name, -+ bin.version AS depend_version, -+ src_pack.name AS depend_src_name -+ FROM -+ ( SELECT name, pkgKey,version, rpm_sourcerpm FROM bin_pack WHERE {} ) bin -+ LEFT JOIN src_pack ON src_pack.src_name = bin.rpm_sourcerpm; -+ """.format(pk_in)) -+ -+ depend_set = data_base.session.execute( -+ sql_bin_src, { -+ 'pkgKey_{}'.format(i): v -+ for i, v in enumerate(bin_src_pkg_key, 1) -+ } -+ ).fetchall() -+ -+ except SQLAlchemyError as sql_err: -+ LOGGER.logger.error(sql_err) -+ except AttributeError as error_msg: -+ LOGGER.logger.error(error_msg) -+ -+ return depend_set -+ -+ # Other methods - def binary_search_database_for_first_time(self, binary_name): - """ - Args: -@@ -553,6 +817,105 @@ class SearchDB(): - - return None, None - -+ def get_src_name(self, binary_name): -+ """ -+ Description: get a package source name from database: -+ bianry_name ->binary_source_name -> source_name -+ 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(): -+ sql_str = """ -+ SELECT DISTINCT -+ src_pack.name AS source_name, -+ src_pack.version AS source_version -+ FROM -+ bin_pack, -+ src_pack -+ WHERE -+ src_pack.src_name = bin_pack.rpm_sourcerpm -+ AND bin_pack.name = :binary_name; -+ """ -+ try: -+ bin_obj = data_base.session.execute(text(sql_str), -+ {"binary_name": binary_name} -+ ).fetchone() -+ source_name = bin_obj.source_name -+ source_version = bin_obj.source_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, None, 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 -+ Args: -+ source_name_list: search package's name, database preority list -+ Returns: -+ response code -+ 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 -+ search_set = {source_name for source_name in source_name_list if source_name} -+ 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 -+ bin_pack.name AS subpack_name, -+ bin_pack.version AS sub_pack_version, -+ src.name AS search_name, -+ src.version AS search_version -+ FROM -+ (SELECT name,version,src_name FROM src_pack WHERE {}) src -+ LEFT JOIN bin_pack on src.src_name = bin_pack.rpm_sourcerpm -+ '''.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 sub_pack_version search_version search_name') -+ for search_name in search_set: -+ result_list.append( -+ (return_tuple(None, None, None, search_name), 'NOT FOUND')) -+ return ResponseCode.SUCCESS, result_list -+ - - def db_priority(): - """ -diff --git a/packageship/application/apps/package/function/self_depend.py b/packageship/application/apps/package/function/self_depend.py -index dd72bed..1ec4c28 100644 ---- a/packageship/application/apps/package/function/self_depend.py -+++ b/packageship/application/apps/package/function/self_depend.py -@@ -8,11 +8,11 @@ class: SelfDepend, DictionaryOperations - - 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 -+from packageship.application.apps.package.function.searchdb import SearchDB -+from packageship.application.apps.package.function.constants import ResponseCode, ListNode -+from packageship.application.apps.package.function.install_depend import InstallDepend \ -+ as install_depend -+from packageship.application.apps.package.function.build_depend import BuildDepend as build_depend - - LOGGER = Log(__name__) - -@@ -35,6 +35,8 @@ class SelfDepend(): - search_db: A object of database which would be connected - not_found_components: Contain the package not found components - """ -+ -+ # pylint: disable = R0902 - def __init__(self, db_list): - """ - init class -@@ -72,7 +74,8 @@ class SelfDepend(): - 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, self.not_found_components -+ return (response_code, self.binary_dict.dictionary, -+ self.source_dicts.dictionary, self.not_found_components) - - for key, _ in self.binary_dict.dictionary.items(): - self.search_install_list.append(key) -@@ -88,7 +91,8 @@ class SelfDepend(): - self.with_subpack() - if self.search_build_list: - self.query_build(selfbuild) -- return response_code, self.binary_dict.dictionary, self.source_dicts.dictionary, self.not_found_components -+ return (response_code, self.binary_dict.dictionary, -+ self.source_dicts.dictionary, self.not_found_components) - - def init_dict(self, packname, packtype): - """ -@@ -105,7 +109,7 @@ class SelfDepend(): - 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': -+ if dbname != 'NOT FOUND': - self.binary_dict.append_bin(key=subpack_tuple.subpack_name, - src=packname, - version=subpack_tuple.search_version, -@@ -155,7 +159,8 @@ class SelfDepend(): - db_, src_version_ = self.search_db.get_version_and_db(source_name) - self.source_dicts.append_src(key=source_name, - dbname=db_ if db_ else values[ListNode.DBNAME], -- version=src_version_ if src_version_ else values[ListNode.VERSION]) -+ version=src_version_ -+ if src_version_ else values[ListNode.VERSION]) - self.search_build_list.append(source_name) - if self.withsubpack == 1: - self.search_subpack_list.append(source_name) -@@ -168,13 +173,14 @@ class SelfDepend(): - Raises: - """ - if None in self.search_subpack_list: -- LOGGER.logger.warning("There is a NONE in input value:" + \ -- str(self.search_subpack_list)) -+ LOGGER.logger.warning("There is a NONE in input value: %s", -+ 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 and subpack_tuple.subpack_name not in self.binary_dict.dictionary: -+ if dbname != 'NOT FOUND': -+ if subpack_tuple.subpack_name and 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.sub_pack_version, -@@ -214,7 +220,7 @@ class SelfDepend(): - 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)) -+ LOGGER.logger.warning("key is NONE for value = %s", str(values)) - continue - if key not in self.binary_dict.dictionary and values[0] != 'source': - self.binary_dict.dictionary[key] = copy.deepcopy(values) -@@ -225,11 +231,13 @@ class SelfDepend(): - db_, src_version_ = self.search_db.get_version_and_db(source_name) - self.source_dicts.append_src(key=source_name, - dbname=db_ if db_ else values[ListNode.DBNAME], -- version=src_version_ if src_version_ else values[ListNode.VERSION]) -+ version=src_version_ -+ if src_version_ else values[ListNode.VERSION]) - if self.withsubpack == 1: - 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]) -+ self.binary_dict.update_value(key=key, -+ parent_list=values[ListNode.PARENT_LIST]) - - def query_selfbuild(self): - """ -@@ -246,7 +254,7 @@ class SelfDepend(): - self.not_found_components.update(not_fd_com) - for key, values in self.result_tmp.items(): - if not key: -- LOGGER.logger.warning("key is NONE for value = " + str(values)) -+ LOGGER.logger.warning("key is NONE for value = %s", str(values)) - continue - if key in self.binary_dict.dictionary: - self.binary_dict.update_value(key=key, parent_list=values[ListNode.PARENT_LIST]) -@@ -255,11 +263,11 @@ class SelfDepend(): - 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)) -+ LOGGER.logger.warning("key is NONE for value = %s", str(values)) - continue - if key not in self.source_dicts.dictionary: - self.source_dicts.dictionary[key] = copy.deepcopy(values) -- if self.with_subpack == 1: -+ if self.withsubpack == 1: - self.search_subpack_list.append(key) - self.search_build_list.clear() - -@@ -289,6 +297,7 @@ class DictionaryOperations(): - """ - self.dictionary[key] = [dbname, version] - -+ # pylint: disable=R0913 - def append_bin(self, key, src=None, version=None, dbname=None, parent_node=None): - """ - Description: Appending binary dictionary -diff --git a/packageship/pkgship b/packageship/pkgship -index e19ddc4..9210bd2 100644 ---- a/packageship/pkgship -+++ b/packageship/pkgship -@@ -20,4 +20,4 @@ if __name__ == '__main__': - main() - except Exception as error: - print('Command execution error please try again ') -- print(e.message) -+ print(error.message) -diff --git a/packageship/pkgshipd b/packageship/pkgshipd -index fef39e3..2035b75 100755 ---- a/packageship/pkgshipd -+++ b/packageship/pkgshipd -@@ -12,23 +12,35 @@ fi - - user=$(id | awk '{print $2}' | cut -d = -f 2) - if [ "$user" == "0(root)" ]; then -- echo "[INFO] Current user is root" -+ echo "[INFO] Current user is root." - else -- echo "[ERROR] Current user is not root, the service don't support common user." -+ echo "[ERROR] Current user is not root." - exit 1 - fi - - function check_config_file(){ - echo "[INFO] Check validation of config file." - check_null -- -+ - echo "[INFO] Check validation of ip addresses." - write_port=$(get_config "$service" "write_port") - query_port=$(get_config "$service" "query_port") - write_ip_addr=$(get_config "$service" "write_ip_addr") - query_ip_addr=$(get_config "$service" "query_ip_addr") -- check_addr $write_ip_addr $write_port -- check_addr $query_ip_addr $query_port -+ if [[ -z $write_ip_addr ]]; then -+ echo "[ERROR] The value of below config names is None in: $SYS_PATH/package.ini, Please check these parameters: write_ip_addr" -+ exit 1 -+ else -+ check_addr $write_ip_addr $write_port -+ fi -+ -+ if [[ -z $query_ip_addr ]]; then -+ echo "[ERROR] The value of below config names is None in: $SYS_PATH/package.ini, Please check these parameters: query_ip_addr" -+ exit 1 -+ else -+ check_addr $query_ip_addr $query_port -+ fi -+ - echo "[INFO] IP addresses are all valid." - - echo "[INFO] Check validation of numbers." -@@ -47,8 +59,8 @@ function check_config_file(){ - echo "[INFO] Check validation of words." - log_level=$(get_config "$service" "log_level") - open=$(get_config "$service" "open") -- check_word $log_level "INFO|DEBUG|WARNING|ERROR|CRITICAL" "log_level" -- check_word $open "True|False" "open" -+ check_word "log_level" "INFO|DEBUG|WARNING|ERROR|CRITICAL" $log_level -+ check_word "open" "True|False" $open - echo "[INFO] All words are valid." - - echo "[INFO] Config file checked valid." -@@ -67,7 +79,7 @@ function check_addr(){ - echo "[ERROR] Invalid ip of $1" - exit 1 - fi -- check_num $2 "port" -+ check_num ${2-"port"} "port" - if [[ $2 -gt 65534 || $2 -lt 1025 ]]; then - echo "[ERROR] Invalid port of $2" - exit 1 -@@ -100,16 +112,21 @@ function check_num(){ - } - - function check_word(){ -- result=`echo $1 | grep -wE "$2"` -+ if [ -z $3 ]; then -+ echo "[ERROR] The value of below config names is None in: $SYS_PATH/package.ini, Please check these parameters: $1" -+ exit 1 -+ fi -+ -+ result=`echo $3 | grep -wE "$2"` - if [ $? -ne 0 ]; then -- echo "[ERROR] $3 should be $2." -+ echo "[ERROR] $1 should be $2." - exit 1 - fi - } - - - function get_config(){ -- cat $SYS_PATH/package.ini | grep -E ^$2 | sed s/[[:space:]]//g | awk 'BEGIN{FS="="}{print $2}' -+ cat $SYS_PATH/package.ini | grep -E ^$2 | sed 's/[[:space:]]//g' | awk 'BEGIN{FS="="}{print $2}' - } - - function create_config_file(){ -@@ -120,12 +137,12 @@ function create_config_file(){ - harakiri=$(get_config "$service" "harakiri") - uwsgi_file_path=$(find /usr/lib/ -name "packageship" | head -n 1) - echo "[INFO] run packageship under path: $uwsgi_file_path" -- if [ $service = "manage" -o $service = "all" ];then -+ 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 "$http_timeout" ]] || [[ -z "$harakiri" ]] || [[ -z "$write_port" ]]; - then -- echo "[ERROR] CAN NOT find all config name in: $SYS_PATH/package.ini, Please check the file" -+ 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, write_ip_addr, harakiri and http-timeout" - exit 1 - fi diff --git a/0002-fix-the-problem-of-continuous-spaces.patch b/0002-fix-the-problem-of-continuous-spaces.patch deleted file mode 100644 index f752a3e..0000000 --- a/0002-fix-the-problem-of-continuous-spaces.patch +++ /dev/null @@ -1,255 +0,0 @@ -diff --git a/packageship/application/initsystem/data_import.py b/packageship/application/initsystem/data_import.py -index c2169c1..a5846bd 100644 ---- a/packageship/application/initsystem/data_import.py -+++ b/packageship/application/initsystem/data_import.py -@@ -84,8 +84,8 @@ class InitDataBase(): - - if not os.path.exists(self.config_file_path): - raise FileNotFoundError( -- 'system initialization configuration file \ -- does not exist: %s' % self.config_file_path) -+ "system initialization configuration file" -+ "does not exist: %s" % self.config_file_path) - # load yaml configuration file - with open(self.config_file_path, 'r', encoding='utf-8') as file_context: - try: -@@ -93,24 +93,25 @@ class InitDataBase(): - file_context.read(), Loader=yaml.FullLoader) - except yaml.YAMLError as yaml_error: - -- raise ConfigurationException(' '.join("The format of the yaml configuration\ -- file is wrong please check and try again:{0}".format(yaml_error).split())) -+ raise ConfigurationException( -+ "The format of the yaml configuration" -+ "file is wrong please check and try again:{0}".format(yaml_error)) - - if init_database_config is None: - raise ConfigurationException( - 'The content of the database initialization configuration file cannot be empty') - if not isinstance(init_database_config, list): - raise ConfigurationException( -- ' '.join('The format of the initial database configuration file\ -- is incorrect.When multiple databases need to be initialized, \ -- it needs to be configured in the form of multiple \ -- nodes:{}'.format(self.config_file_path).split())) -+ "The format of the initial database configuration file" -+ "is incorrect.When multiple databases need to be initialized," -+ "it needs to be configured in the form of multiple" -+ "nodes:{}".format(self.config_file_path)) - for config_item in init_database_config: - if not isinstance(config_item, dict): -- raise ConfigurationException(' '.join('The format of the initial database\ -- configuration file is incorrect, and the value in a single node should\ -- be presented in the form of key - val pairs: \ -- {}'.format(self.config_file_path).split())) -+ raise ConfigurationException( -+ "The format of the initial database" -+ "configuration file is incorrect, and the value in a single node should" -+ "be presented in the form of key - val pairs:{}".format(self.config_file_path)) - return init_database_config - - def init_data(self): -@@ -122,8 +123,8 @@ class InitDataBase(): - """ - if getattr(self, 'config_file_datas', None) is None or \ - self.config_file_datas is None: -- raise ContentNoneException('The content of the database initialization \ -- configuration file is empty') -+ raise ContentNoneException("The content of the database initialization" -+ "configuration file is empty") - - if self.__exists_repeat_database(): - raise DatabaseRepeatException( -@@ -139,13 +140,13 @@ class InitDataBase(): - continue - priority = database_config.get('priority') - if not isinstance(priority, int) or priority < 0 or priority > 100: -- LOGGER.logger.error('The priority value type in the database initialization \ -- configuration file is incorrect') -+ LOGGER.logger.error("The priority value type in the database initialization" -+ "configuration file is incorrect") - continue - lifecycle_status_val = database_config.get('lifecycle') - if lifecycle_status_val not in ('enable', 'disable'): -- LOGGER.logger.error('The status value of the life cycle in the initialization\ -- configuration file can only be enable or disable') -+ LOGGER.logger.error("The value of the life cycle in the initialization" -+ "configuration file can only be enable or disable") - continue - # Initialization data - self._init_data(database_config) -@@ -163,8 +164,8 @@ class InitDataBase(): - """ - _database_engine = self._database_engine.get(self.db_type) - if not _database_engine: -- raise Error('The database engine is set incorrectly, \ -- currently only the following engines are supported: %s ' -+ raise Error("The database engine is set incorrectly," -+ "currently only the following engines are supported: %s " - % '、'.join(self._database_engine.keys())) - _create_table_result = _database_engine( - db_name=db_name, tables=tables, storage=storage).create_database(self) -@@ -200,11 +201,12 @@ class InitDataBase(): - - if src_db_file is None or bin_db_file is None: - raise ContentNoneException( -- 'The path to the sqlite file in the database initialization configuration \ -- is incorrect ') -+ "The path to the sqlite file in the database initialization" -+ "configuration is incorrect ") - if not os.path.exists(src_db_file) or not os.path.exists(bin_db_file): -- raise FileNotFoundError("sqlite file {src} or {bin} does not exist, please \ -- check and try again".format(src=src_db_file, bin=bin_db_file)) -+ raise FileNotFoundError( -+ "sqlite file {src} or {bin} does not exist, please" -+ "check and try again".format(src=src_db_file, bin=bin_db_file)) - # 3. Obtain temporary source package files and binary package files - if self.__save_data(database_config, - self.database_name): -@@ -314,23 +316,20 @@ class InitDataBase(): - - Args: - db_name: Saved database name -- Returns: -- -- Raises: -- - """ - # Query all source packages - self.sql = " select * from packages " - packages_datas = self.__get_data() - if packages_datas is None: - raise ContentNoneException( -- '{db_name}:There is no relevant data in the source \ -- package provided '.format(db_name=db_name)) -+ "{db_name}:There is no relevant data in the source " -+ "package provided ".format(db_name=db_name)) - for index, src_package_item in enumerate(packages_datas): - try: - src_package_name = '-'.join([src_package_item.get('name'), - src_package_item.get('version'), -- src_package_item.get('release') + '.src.rpm' -+ src_package_item.get( -+ 'release') + '.src.rpm' - ]) - except AttributeError as exception_msg: - src_package_name = None -@@ -391,8 +390,9 @@ class InitDataBase(): - self.sql = " select * from requires " - requires_datas = self.__get_data() - if requires_datas is None: -- raise ContentNoneException('{db_name}: The package data that the source package \ -- depends on is empty'.format(db_name=db_name)) -+ raise ContentNoneException( -+ "{db_name}: The package data that the source package " -+ "depends on is empty".format(db_name=db_name)) - with DBHelper(db_name=db_name) as database: - database.batch_add(requires_datas, SrcRequires) - -@@ -411,8 +411,8 @@ class InitDataBase(): - bin_packaegs = self.__get_data() - if bin_packaegs is None: - raise ContentNoneException( -- '{db_name}:There is no relevant data in the provided \ -- binary package '.format(db_name=db_name)) -+ "{db_name}:There is no relevant data in the provided " -+ "binary package ".format(db_name=db_name)) - for index, bin_package_item in enumerate(bin_packaegs): - try: - src_package_name = bin_package_item.get('rpm_sourcerpm').split( -@@ -441,8 +441,8 @@ class InitDataBase(): - requires_datas = self.__get_data() - if requires_datas is None: - raise ContentNoneException( -- '{db_name}:There is no relevant data in the provided binary \ -- dependency package'.format(db_name=db_name)) -+ "{db_name}:There is no relevant data in the provided binary " -+ "dependency package".format(db_name=db_name)) - - with DBHelper(db_name=db_name) as database: - database.batch_add(requires_datas, BinRequires) -@@ -462,8 +462,8 @@ class InitDataBase(): - provides_datas = self.__get_data() - if provides_datas is None: - raise ContentNoneException( -- '{db_name}:There is no relevant data in the provided \ -- binary component '.format(db_name=db_name)) -+ "{db_name}:There is no relevant data in the provided " -+ "binary component ".format(db_name=db_name)) - - with DBHelper(db_name=db_name) as database: - database.batch_add(provides_datas, BinProvides) -@@ -474,8 +474,8 @@ class InitDataBase(): - files_datas = self.__get_data() - if files_datas is None: - raise ContentNoneException( -- '{db_name}:There is no relevant binary file installation\ -- path data in the provided database '.format(db_name=db_name)) -+ "{db_name}:There is no relevant binary file installation " -+ "path data in the provided database ".format(db_name=db_name)) - - with DBHelper(db_name=db_name) as database: - database.batch_add(files_datas, BinFiles) -diff --git a/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/libs/dbutils/sqlalchemy_helper.py -index a0b22e2..d18b115 100644 ---- a/packageship/libs/dbutils/sqlalchemy_helper.py -+++ b/packageship/libs/dbutils/sqlalchemy_helper.py -@@ -279,8 +279,8 @@ class DBHelper(BaseHelper): - - 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') -+ "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(), -diff --git a/packageship/pkgship.py b/packageship/pkgship.py -index 884b2ab..f9408c8 100644 ---- a/packageship/pkgship.py -+++ b/packageship/pkgship.py -@@ -25,8 +25,8 @@ try: - - LOGGER = Log(__name__) - except ImportError as import_error: -- print('Error importing related dependencies, \ -- please check if related dependencies are installed') -+ 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 -@@ -230,7 +230,9 @@ class PkgshipCommand(BaseCommand): - if package_all.get("not_found_components"): - print("Problem: Not Found Components") - for not_found_com in package_all.get("not_found_components"): -- print(" - nothing provides {} needed by {} ".format(not_found_com, params.packagename)) -+ print( -+ " - nothing provides {} needed by {} ". -+ format(not_found_com, params.packagename)) - package_all = package_all.get("build_dict") - - for bin_package, package_depend in package_all.items(): -@@ -835,7 +837,9 @@ class InstallDepCommand(PkgshipCommand): - if package_all.get("not_found_components"): - print("Problem: Not Found Components") - for not_found_com in package_all.get("not_found_components"): -- print(" - nothing provides {} needed by {} ".format(not_found_com, params.packagename)) -+ print( -+ " - nothing provides {} needed by {} ". -+ format(not_found_com, params.packagename)) - for bin_package, package_depend in package_all.get("install_dict").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': -@@ -1061,7 +1065,9 @@ class SelfBuildCommand(PkgshipCommand): - if package_all.get("not_found_components"): - print("Problem: Not Found Components") - for not_found_com in package_all.get("not_found_components"): -- print(" - nothing provides {} needed by {} ".format(not_found_com, params.packagename)) -+ print( -+ " - nothing provides {} needed by {} ". -+ format(not_found_com, params.packagename)) - bin_package_count = self._parse_bin_package( - package_all.get('binary_dicts')) - diff --git a/0003-fix-log_level-configuration-item-not-work.patch b/0003-fix-log_level-configuration-item-not-work.patch deleted file mode 100644 index 3d4b564..0000000 --- a/0003-fix-log_level-configuration-item-not-work.patch +++ /dev/null @@ -1,55 +0,0 @@ -diff --git a/packageship/application/__init__.py b/packageship/application/__init__.py -index 1361058..6a57a2e 100644 ---- a/packageship/application/__init__.py -+++ b/packageship/application/__init__.py -@@ -2,8 +2,6 @@ - """ - Initial operation and configuration of the flask project - """ --import sys --import threading - from flask import Flask - from flask_session import Session - from flask_apscheduler import APScheduler -@@ -19,7 +17,9 @@ def _timed_task(app): - """ - Timed task function - """ -- from .apps.lifecycle.function.download_yaml import update_pkg_info # pylint: disable=import-outside-toplevel -+ # disable=import-outside-toplevel Avoid circular import problems,so import inside the function -+ # pylint: disable=import-outside-toplevel -+ from packageship.application.apps.lifecycle.function.download_yaml import update_pkg_info - - _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) - try: -@@ -34,6 +34,7 @@ def _timed_task(app): - if _minute < 0 or _minute > 59: - _minute = 0 - -+ # disable=no-member Dynamic variable pylint is not recognized - app.apscheduler.add_job( # pylint: disable=no-member - func=update_pkg_info, id="update_package_data", trigger="cron", hour=_hour, minute=_minute) - app.apscheduler.add_job( # pylint: disable=no-member -@@ -52,7 +53,8 @@ def init_app(operation): - app = Flask(__name__) - - # log configuration -- app.logger.addHandler(setup_log(Config)) -+ # disable=no-member Dynamic variable pylint is not recognized -+ app.logger.addHandler(setup_log(Config())) # pylint: disable=no-member - - # Load configuration items - -@@ -66,10 +68,12 @@ def init_app(operation): - # Open session function - Session(app) - -+ # Variables OPERATION need to be modified within the function and imported in other modules - global OPERATION # pylint: disable=global-statement - OPERATION = operation - - # Register Blueprint -+ # disable=import-outside-toplevel Avoid circular import problems,so import inside the function - from packageship.application import apps # pylint: disable=import-outside-toplevel - for blue, api in apps.blue_point: - api.init_app(app) diff --git a/0004-fix-the-error-when-executing-query-commands.patch b/0004-fix-the-error-when-executing-query-commands.patch deleted file mode 100644 index 04552e5..0000000 --- a/0004-fix-the-error-when-executing-query-commands.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/packageship/application/apps/package/function/packages.py b/packageship/application/apps/package/function/packages.py -index eb96087..d36fc34 100644 ---- a/packageship/application/apps/package/function/packages.py -+++ b/packageship/application/apps/package/function/packages.py -@@ -313,7 +313,8 @@ def _sub_pack(src_name, table_name): - pro_info = res[pro_obj.sub_name]["provides"] - if pro_obj.sub_pro_name in pro_info: - pro_info[pro_obj.sub_pro_name]["requiredby"].update( -- {pro_obj.sub_reqby_name: pro_obj.sub_reqby_name}) -+ {pro_obj.sub_reqby_name: pro_obj.sub_reqby_name} -+ if pro_obj.sub_reqby_name else {}) - else: - pro_info.update( - { -@@ -368,7 +369,8 @@ def _sub_pack(src_name, table_name): - req_info = sub_pkg_info["requires"] - if req_obj.sub_req_name in req_info: - req_info[req_obj.sub_req_name]["providedby"].update( -- {req_obj.sub_proby_name: req_obj.sub_proby_name}) -+ {req_obj.sub_proby_name: req_obj.sub_proby_name} -+ if req_obj.sub_proby_name else {}) - else: - req_info.update( - { diff --git a/0005-fix-the-error-when-source-package-has-no-sub-packages.patch b/0005-fix-the-error-when-source-package-has-no-sub-packages.patch deleted file mode 100644 index 6188dbf..0000000 --- a/0005-fix-the-error-when-source-package-has-no-sub-packages.patch +++ /dev/null @@ -1,62 +0,0 @@ -diff --git a/packageship/application/apps/package/function/self_depend.py b/packageship/application/apps/package/function/self_depend.py -index 1ec4c28..b06b950 100644 ---- a/packageship/application/apps/package/function/self_depend.py -+++ b/packageship/application/apps/package/function/self_depend.py -@@ -106,16 +106,20 @@ class SelfDepend(): - """ - 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 -+ if not subpack_list: -+ return ResponseCode.PACK_NAME_NOT_FOUND -+ -+ for subpack_tuple, dbname in subpack_list: -+ self.source_dicts.append_src(packname, dbname, subpack_tuple.search_version) -+ if dbname == 'NOT FOUND': -+ continue -+ -+ if subpack_tuple.subpack_name and subpack_tuple.subpack_name \ -+ not in self.binary_dict.dictionary: -+ self.binary_dict.append_bin(key=subpack_tuple.subpack_name, -+ src=packname, -+ version=subpack_tuple.search_version, -+ dbname=dbname) - - else: - response_code, dbname, source_name, version = \ -@@ -178,15 +182,17 @@ class SelfDepend(): - 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 and 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.sub_pack_version, -- dbname=dbname, -- parent_node=[subpack_tuple.search_name, 'Subpack']) -- self.search_install_list.append(subpack_tuple.subpack_name) -+ if dbname == 'NOT FOUND': -+ continue -+ -+ if subpack_tuple.subpack_name and 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.sub_pack_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): diff --git a/0006-fix-memory_caused-service-crash-and-data-duplication-issue.patch b/0006-fix-memory_caused-service-crash-and-data-duplication-issue.patch deleted file mode 100644 index 0e4ee66..0000000 --- a/0006-fix-memory_caused-service-crash-and-data-duplication-issue.patch +++ /dev/null @@ -1,3055 +0,0 @@ -diff -Naru a/packageship/application/apps/lifecycle/function/concurrent.py b/packageship/application/apps/lifecycle/function/concurrent.py ---- a/packageship/application/apps/lifecycle/function/concurrent.py 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/application/apps/lifecycle/function/concurrent.py 2020-09-22 23:48:39.938515522 +0800 -@@ -1,65 +1,76 @@ --#!/usr/bin/python3 --""" --Use queues to implement the producer and consumer model --to solve the database lock introduced by high concurrency issues --""" --import threading --from queue import Queue --from sqlalchemy.exc import SQLAlchemyError --from packageship.libs.dbutils import DBHelper --from packageship.libs.exception import Error, ContentNoneException --from packageship.libs.log import Log -- -- --class ProducerConsumer(): -- """ -- The data written in the database is added to the high -- concurrency queue, and the high concurrency is solved -- by the form of the queue -- """ -- _queue = Queue(maxsize=0) -- _instance_lock = threading.Lock() -- _log = Log(__name__) -- -- def __init__(self): -- self.thread_queue = threading.Thread(target=self.__queue_process) -- if not self.thread_queue.isAlive(): -- self.thread_queue.start() -- -- def start_thread(self): -- """ -- Judge a thread, if the thread is terminated, restart -- """ -- if not self.thread_queue.isAlive(): -- self.thread_queue = threading.Thread(target=self.__queue_process) -- self.thread_queue.start() -- -- def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument -- """ -- Use the singleton pattern to create a thread-safe producer pattern -- """ -- if not hasattr(cls, "_instance"): -- with cls._instance_lock: -- if not hasattr(cls, "_instance"): -- cls._instance = object.__new__(cls) -- return cls._instance -- -- def __queue_process(self): -- """ -- Read the content in the queue and save and update -- """ -- while not self._queue.empty(): -- _queue_value = self._queue.get() -- try: -- with DBHelper(db_name="lifecycle") as database: -- database.add(_queue_value) -- except (Error, ContentNoneException, SQLAlchemyError) as error: -- self._log.logger.error(error) -- -- def put(self, pending_content): -- """ -- The content of the operation is added to the queue -- """ -- if pending_content: -- self._queue.put(pending_content) -- self.start_thread() -+#!/usr/bin/python3 -+""" -+Use queues to implement the producer and consumer model -+to solve the database lock introduced by high concurrency issues -+""" -+import threading -+import time -+from queue import Queue -+from sqlalchemy.exc import SQLAlchemyError -+from sqlalchemy.exc import OperationalError -+from packageship.libs.exception import Error, ContentNoneException -+from packageship.libs.log import Log -+from packageship.libs.configutils.readconfig import ReadConfig -+from packageship import system_config -+ -+ -+class ProducerConsumer(): -+ """ -+ The data written in the database is added to the high -+ concurrency queue, and the high concurrency is solved -+ by the form of the queue -+ """ -+ _queue = Queue(maxsize=1000) -+ _instance_lock = threading.Lock() -+ _log = Log(__name__) -+ -+ def __init__(self): -+ self.thread_queue = threading.Thread(target=self.__queue_process) -+ self._instance_lock.acquire() -+ if not self.thread_queue.isAlive(): -+ self.thread_queue = threading.Thread(target=self.__queue_process) -+ self.thread_queue.start() -+ self._instance_lock.release() -+ -+ def start_thread(self): -+ """ -+ Judge a thread, if the thread is terminated, restart -+ """ -+ self._instance_lock.acquire() -+ if not self.thread_queue.isAlive(): -+ self.thread_queue = threading.Thread(target=self.__queue_process) -+ self.thread_queue.start() -+ self._instance_lock.release() -+ -+ def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument -+ """ -+ Use the singleton pattern to create a thread-safe producer pattern -+ """ -+ if not hasattr(cls, "_instance"): -+ with cls._instance_lock: -+ if not hasattr(cls, "_instance"): -+ cls._instance = object.__new__(cls) -+ return cls._instance -+ -+ def __queue_process(self): -+ """ -+ Read the content in the queue and save and update -+ """ -+ while not self._queue.empty(): -+ _queue_value, method = self._queue.get() -+ try: -+ method(_queue_value) -+ except OperationalError as error: -+ self._log.logger.warning(error) -+ time.sleep(0.2) -+ self._queue.put((_queue_value, method)) -+ except (Error, ContentNoneException, SQLAlchemyError) as error: -+ self._log.logger.error(error) -+ -+ def put(self, pending_content): -+ """ -+ The content of the operation is added to the queue -+ """ -+ if pending_content: -+ self._queue.put(pending_content) -+ self.start_thread() -diff -Naru a/packageship/application/apps/lifecycle/function/download_yaml.py b/packageship/application/apps/lifecycle/function/download_yaml.py ---- a/packageship/application/apps/lifecycle/function/download_yaml.py 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/application/apps/lifecycle/function/download_yaml.py 2020-09-22 23:48:46.478549707 +0800 -@@ -1,222 +1,224 @@ --#!/usr/bin/python3 --""" --Dynamically obtain the content of the yaml file \ --that saves the package information, periodically \ --obtain the content and save it in the database --""" --import copy --from concurrent.futures import ThreadPoolExecutor --import datetime as date --import requests --import yaml --from retrying import retry --from sqlalchemy.exc import SQLAlchemyError --from requests.exceptions import HTTPError --from packageship import system_config --from packageship.application.models.package import Packages --from packageship.application.models.package import PackagesMaintainer --from packageship.libs.dbutils import DBHelper --from packageship.libs.exception import Error, ContentNoneException --from packageship.libs.configutils.readconfig import ReadConfig --from .base import Base --from .gitee import Gitee --from .concurrent import ProducerConsumer -- -- --class ParseYaml(): -- """ -- Description: Analyze the downloaded remote yaml file, obtain the tags -- and maintainer information in the yaml file, and save the obtained -- relevant information into the database -- -- Attributes: -- base: base class instance -- pkg: Specific package data -- _table_name: The name of the data table to be operated -- openeuler_advisor_url: Get the warehouse address of the yaml file -- _yaml_content: The content of the yaml file -- """ -- -- def __init__(self, pkg_info, base, table_name): -- self.base = base -- self.pkg = pkg_info -- self._table_name = table_name -- self.openeuler_advisor_url = self._path_stitching(pkg_info.name) -- self._yaml_content = None -- self.timed_task_open = self._timed_task_status() -- self.producer_consumer = ProducerConsumer() -- -- def _timed_task_status(self): -- """ -- The open state of information such as the maintainer in the scheduled task -- """ -- _timed_task_status = True -- _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -- open_status = _readconfig.get_config('TIMEDTASK', 'open') -- if open_status not in ('True', 'False'): -- self.base.log.logger.error( -- 'Wrong setting of the open state value of the scheduled task') -- if open_status == 'False': -- self.timed_task_open = False -- return _timed_task_status -- -- def _path_stitching(self, pkg_name): -- """ -- The path of the remote service call -- """ -- _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -- _remote_url = _readconfig.get_config('LIFECYCLE', 'warehouse_remote') -- if _remote_url is None: -- _remote_url = 'https://gitee.com/openeuler/openEuler-Advisor/raw/master/upstream-info/' -- return _remote_url + '{pkg_name}.yaml'.format(pkg_name=pkg_name) -- -- def update_database(self): -- """ -- For the current package, determine whether the specific yaml file exists, parse -- the data in it and save it in the database if it exists, and record the relevant -- log if it does not exist -- -- """ -- if self._openeuler_advisor_exists_yaml(): -- self._save_to_database() -- else: -- msg = "The yaml information of the [%s] package has not been" \ -- "obtained yet" % self.pkg.name -- self.base.log.logger.warning(msg) -- -- def _get_yaml_content(self, url): -- """ -- -- """ -- try: -- response = requests.get( -- url, headers=self.base.headers) -- if response.status_code == 200: -- self._yaml_content = yaml.safe_load(response.content) -- -- except HTTPError as error: -- self.base.log.logger.error(error) -- -- def _openeuler_advisor_exists_yaml(self): -- """ -- Determine whether there is a yaml file with the current \ -- package name under the openeuler-advisor project -- -- """ -- self._get_yaml_content(self.openeuler_advisor_url) -- if self._yaml_content: -- return True -- return False -- -- def _save_to_database(self): -- """ -- Save the acquired yaml file information to the database -- -- Raises: -- ContentNoneException: The added entity content is empty -- Error: An error occurred during data addition -- """ -- self._parse_warehouse_info() -- tags = self._yaml_content.get('git_tag', None) -- if tags: -- self._parse_tags_content(tags) -- self.producer_consumer.put(copy.deepcopy(self.pkg)) -- if self.timed_task_open: -- _maintainer = self._yaml_content.get('maintainers') -- if _maintainer and isinstance(_maintainer, list): -- self.pkg.maintainer = _maintainer[0] -- self.pkg.maintainlevel = self._yaml_content.get('maintainlevel') -- try: -- if self.timed_task_open: -- @retry(stop_max_attempt_number=3, stop_max_delay=500) -- def _save_maintainer_info(): -- with DBHelper(db_name="lifecycle") as database: -- _packages_maintainer = database.session.query( -- PackagesMaintainer).filter( -- PackagesMaintainer.name == self.pkg.name).first() -- if _packages_maintainer: -- _packages_maintainer.name = self.pkg.name -- _packages_maintainer.maintainer = self.pkg.maintainer -- _packages_maintainer.maintainlevel = self.pkg.maintainlevel -- else: -- _packages_maintainer = PackagesMaintainer( -- name=self.pkg.name, maintainer=self.pkg.maintainer, -- maintainlevel=self.pkg.maintainlevel) -- self.producer_consumer.put( -- copy.deepcopy(_packages_maintainer)) -- _save_maintainer_info() -- except (Error, ContentNoneException, SQLAlchemyError) as error: -- self.base.log.logger.error(error) -- -- def _parse_warehouse_info(self): -- """ -- Parse the warehouse information in the yaml file -- -- """ -- if self._yaml_content: -- self.pkg.version_control = self._yaml_content.get( -- 'version_control') -- self.pkg.src_repo = self._yaml_content.get('src_repo') -- self.pkg.tag_prefix = self._yaml_content.get('tag_prefix') -- -- def _parse_tags_content(self, tags): -- """ -- Parse the obtained tags content -- -- """ -- try: -- # Integrate tags information into key-value pairs -- _tags = [(tag.split()[0], tag.split()[1]) for tag in tags] -- _tags = sorted(_tags, key=lambda x: x[0], reverse=True) -- self.pkg.latest_version = _tags[0][1] -- self.pkg.latest_version_time = _tags[0][0] -- _end_time = date.datetime.strptime( -- self.pkg.latest_version_time, '%Y-%m-%d') -- if self.pkg.latest_version != self.pkg.version: -- for _version in _tags: -- if _version[1] == self.pkg.version: -- _end_time = date.datetime.strptime( -- _version[0], '%Y-%m-%d') -- self.pkg.used_time = (date.datetime.now() - _end_time).days -- -- except (IndexError, Error) as index_error: -- self.base.log.logger.error(index_error) -- -- --def update_pkg_info(pkg_info_update=True): -- """ -- Update the information of the upstream warehouse in the source package -- -- """ -- try: -- base_control = Base() -- _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -- pool_workers = _readconfig.get_config('LIFECYCLE', 'pool_workers') -- _warehouse = _readconfig.get_config('LIFECYCLE', 'warehouse') -- if _warehouse is None: -- _warehouse = 'src-openeuler' -- if not isinstance(pool_workers, int): -- pool_workers = 10 -- # Open thread pool -- pool = ThreadPoolExecutor(max_workers=pool_workers) -- with DBHelper(db_name="lifecycle") as database: -- for table_name in filter(lambda x: x not in ['packages_issue', 'packages_maintainer'], -- database.engine.table_names()): -- -- cls_model = Packages.package_meta(table_name) -- # Query a specific table -- for package_item in database.session.query(cls_model).all(): -- if pkg_info_update: -- parse_yaml = ParseYaml( -- pkg_info=copy.deepcopy(package_item), -- base=base_control, -- table_name=table_name) -- pool.submit(parse_yaml.update_database) -- else: -- # Get the issue of each warehouse and save it -- gitee_issue = Gitee( -- package_item, _warehouse, package_item.name, table_name) -- pool.submit(gitee_issue.query_issues_info) -- pool.shutdown() -- except SQLAlchemyError as error_msg: -- base_control.log.logger.error(error_msg) -+#!/usr/bin/python3 -+""" -+Dynamically obtain the content of the yaml file \ -+that saves the package information, periodically \ -+obtain the content and save it in the database -+""" -+import copy -+from concurrent.futures import ThreadPoolExecutor -+import datetime as date -+import requests -+import yaml -+from retrying import retry -+from sqlalchemy.exc import SQLAlchemyError -+from requests.exceptions import HTTPError -+from packageship import system_config -+from packageship.application.models.package import Packages -+from packageship.application.models.package import PackagesMaintainer -+from packageship.libs.dbutils import DBHelper -+from packageship.libs.exception import Error, ContentNoneException -+from packageship.libs.configutils.readconfig import ReadConfig -+from .base import Base -+from .gitee import Gitee -+from .concurrent import ProducerConsumer -+ -+ -+class ParseYaml(): -+ """ -+ Description: Analyze the downloaded remote yaml file, obtain the tags -+ and maintainer information in the yaml file, and save the obtained -+ relevant information into the database -+ -+ Attributes: -+ base: base class instance -+ pkg: Specific package data -+ _table_name: The name of the data table to be operated -+ openeuler_advisor_url: Get the warehouse address of the yaml file -+ _yaml_content: The content of the yaml file -+ """ -+ -+ def __init__(self, pkg_info, base, table_name): -+ self.base = base -+ self.pkg = pkg_info -+ self._table_name = table_name -+ self.openeuler_advisor_url = self._path_stitching(pkg_info.name) -+ self._yaml_content = None -+ self.timed_task_open = self._timed_task_status() -+ self.producer_consumer = ProducerConsumer() -+ -+ def _timed_task_status(self): -+ """ -+ The open state of information such as the maintainer in the scheduled task -+ """ -+ _timed_task_status = True -+ _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -+ open_status = _readconfig.get_config('TIMEDTASK', 'open') -+ if open_status not in ('True', 'False'): -+ self.base.log.logger.error( -+ 'Wrong setting of the open state value of the scheduled task') -+ if open_status == 'False': -+ self.timed_task_open = False -+ return _timed_task_status -+ -+ def _path_stitching(self, pkg_name): -+ """ -+ The path of the remote service call -+ """ -+ _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -+ _remote_url = _readconfig.get_config('LIFECYCLE', 'warehouse_remote') -+ if _remote_url is None: -+ _remote_url = 'https://gitee.com/openeuler/openEuler-Advisor/raw/master/upstream-info/' -+ return _remote_url + '{pkg_name}.yaml'.format(pkg_name=pkg_name) -+ -+ def update_database(self): -+ """ -+ For the current package, determine whether the specific yaml file exists, parse -+ the data in it and save it in the database if it exists, and record the relevant -+ log if it does not exist -+ -+ """ -+ if self._openeuler_advisor_exists_yaml(): -+ self._save_to_database() -+ else: -+ msg = "The yaml information of the [%s] package has not been" \ -+ "obtained yet" % self.pkg.name -+ self.base.log.logger.warning(msg) -+ -+ def _get_yaml_content(self, url): -+ """ -+ -+ """ -+ try: -+ response = requests.get( -+ url, headers=self.base.headers) -+ if response.status_code == 200: -+ self._yaml_content = yaml.safe_load(response.content) -+ -+ except HTTPError as error: -+ self.base.log.logger.error(error) -+ -+ def _openeuler_advisor_exists_yaml(self): -+ """ -+ Determine whether there is a yaml file with the current \ -+ package name under the openeuler-advisor project -+ -+ """ -+ self._get_yaml_content(self.openeuler_advisor_url) -+ if self._yaml_content: -+ return True -+ return False -+ -+ def _save_to_database(self): -+ """ -+ Save the acquired yaml file information to the database -+ -+ Raises: -+ ContentNoneException: The added entity content is empty -+ Error: An error occurred during data addition -+ """ -+ -+ def _save_package(package_module): -+ with DBHelper(db_name="lifecycle") as database: -+ database.add(package_module) -+ -+ def _save_maintainer_info(maintainer_module): -+ with DBHelper(db_name="lifecycle") as database: -+ _packages_maintainer = database.session.query( -+ PackagesMaintainer).filter( -+ PackagesMaintainer.name == maintainer_module['name']).first() -+ if _packages_maintainer: -+ for key, val in maintainer_module.items(): -+ setattr(_packages_maintainer, key, val) -+ else: -+ _packages_maintainer = PackagesMaintainer( -+ **maintainer_module) -+ database.add(_packages_maintainer) -+ -+ self._parse_warehouse_info() -+ tags = self._yaml_content.get('git_tag', None) -+ if tags: -+ self._parse_tags_content(tags) -+ self.producer_consumer.put( -+ (copy.deepcopy(self.pkg), _save_package)) -+ if self.timed_task_open: -+ maintainer = {'name': self.pkg.name} -+ _maintainer = self._yaml_content.get('maintainers') -+ if _maintainer and isinstance(_maintainer, list): -+ maintainer['maintainer'] = _maintainer[0] -+ maintainer['maintainlevel'] = self._yaml_content.get( -+ 'maintainlevel') -+ -+ self.producer_consumer.put((maintainer, _save_maintainer_info)) -+ -+ def _parse_warehouse_info(self): -+ """ -+ Parse the warehouse information in the yaml file -+ -+ """ -+ if self._yaml_content: -+ self.pkg.version_control = self._yaml_content.get( -+ 'version_control') -+ self.pkg.src_repo = self._yaml_content.get('src_repo') -+ self.pkg.tag_prefix = self._yaml_content.get('tag_prefix') -+ -+ def _parse_tags_content(self, tags): -+ """ -+ Parse the obtained tags content -+ -+ """ -+ try: -+ # Integrate tags information into key-value pairs -+ _tags = [(tag.split()[0], tag.split()[1]) for tag in tags] -+ _tags = sorted(_tags, key=lambda x: x[0], reverse=True) -+ self.pkg.latest_version = _tags[0][1] -+ self.pkg.latest_version_time = _tags[0][0] -+ _end_time = date.datetime.strptime( -+ self.pkg.latest_version_time, '%Y-%m-%d') -+ if self.pkg.latest_version != self.pkg.version: -+ for _version in _tags: -+ if _version[1] == self.pkg.version: -+ _end_time = date.datetime.strptime( -+ _version[0], '%Y-%m-%d') -+ self.pkg.used_time = (date.datetime.now() - _end_time).days -+ -+ except (IndexError, Error) as index_error: -+ self.base.log.logger.error(index_error) -+ -+ -+def update_pkg_info(pkg_info_update=True): -+ """ -+ Update the information of the upstream warehouse in the source package -+ -+ """ -+ try: -+ base_control = Base() -+ _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -+ pool_workers = _readconfig.get_config('LIFECYCLE', 'pool_workers') -+ _warehouse = _readconfig.get_config('LIFECYCLE', 'warehouse') -+ if _warehouse is None: -+ _warehouse = 'src-openeuler' -+ if not isinstance(pool_workers, int): -+ pool_workers = 10 -+ # Open thread pool -+ pool = ThreadPoolExecutor(max_workers=pool_workers) -+ with DBHelper(db_name="lifecycle") as database: -+ for table_name in filter(lambda x: x not in ['packages_issue', 'packages_maintainer', 'database_info'], -+ database.engine.table_names()): -+ -+ cls_model = Packages.package_meta(table_name) -+ # Query a specific table -+ for package_item in database.session.query(cls_model).all(): -+ if pkg_info_update: -+ parse_yaml = ParseYaml( -+ pkg_info=copy.deepcopy(package_item), -+ base=base_control, -+ table_name=table_name) -+ pool.submit(parse_yaml.update_database) -+ else: -+ # Get the issue of each warehouse and save it -+ gitee_issue = Gitee( -+ copy.deepcopy(package_item), _warehouse, package_item.name, table_name) -+ pool.submit(gitee_issue.query_issues_info) -+ pool.shutdown() -+ except SQLAlchemyError as error_msg: -+ base_control.log.logger.error(error_msg) -diff -Naru a/packageship/application/apps/lifecycle/function/gitee.py b/packageship/application/apps/lifecycle/function/gitee.py ---- a/packageship/application/apps/lifecycle/function/gitee.py 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/application/apps/lifecycle/function/gitee.py 2020-09-22 23:48:52.698582219 +0800 -@@ -1,224 +1,223 @@ --#!/usr/bin/python3 --""" --Description:Get issue info from gitee --Class: Gitee --""" --import copy --from json import JSONDecodeError --from retrying import retry --import requests --from requests.exceptions import HTTPError --from sqlalchemy.exc import SQLAlchemyError --from packageship.libs.dbutils import DBHelper --from packageship.libs.configutils.readconfig import ReadConfig --from packageship.libs.exception import Error, ContentNoneException --from packageship.application.models.package import PackagesIssue --from packageship import system_config --from packageship.libs.log import Log --from .concurrent import ProducerConsumer -- --LOGGER = Log(__name__) -- -- --class Gitee(): -- """ -- gitee version management tool related information acquisition -- -- """ -- -- def __init__(self, pkg_info, owner, repo, table_name): -- self.pkg_info = pkg_info -- self.owner = owner -- self.repo = repo -- self._read_config = ReadConfig(system_config.SYS_CONFIG_PATH) -- self.url = "https://gitee.com/" -- self.api_url = "https://gitee.com/api/v5/repos" -- self.pool = None -- self.issue_id = None -- self.defect = 0 -- self.feature = 0 -- self.cve = 0 -- self.patch_files_path = self._read_config.get_system( -- "patch_files_path") -- self.table_name = table_name -- self.producer_consumer = ProducerConsumer() -- -- def query_issues_info(self, issue_id=""): -- """ -- Description: View the issue details of the specified package -- Args: -- issue_id: Issue id -- Returns: -- issue_content_list: The issue details of the specified package list -- Raises: -- -- """ -- issue_url = self.api_url + \ -- "/{}/{}/issues/{}".format(self.owner, self.repo, issue_id) -- try: -- response = requests.get( -- issue_url, params={"state": "all", "per_page": 100}) -- except Error as error: -- LOGGER.logger.error(error) -- return None -- if response.status_code != 200: -- return None -- total_page = 1 if issue_id else int(response.headers['total_page']) -- total_count = int(response.headers['total_count']) -- if total_count > 0: -- issue_list = self._query_per_page_issue_info(total_page, issue_url) -- if not issue_list: -- LOGGER.logger.error( -- "An error occurred while querying {}".format(self.repo)) -- return None -- self._save_issues(issue_list) -- -- def _query_per_page_issue_info(self, total_page, issue_url): -- """ -- Description: View the issue details -- Args: -- total_page: total page -- issue_url: issue url -- -- Returns: -- -- """ -- issue_content_list = [] -- for i in range(1, total_page + 1): -- -- @retry(stop_max_attempt_number=3, stop_max_delay=1000) -- def request_issue(page, issue_url): -- try: -- response = requests.get(issue_url, -- params={"state": "all", "per_page": 100, "page": page}) -- except HTTPError: -- raise HTTPError('Network request error') -- return response -- -- try: -- response = request_issue(i, issue_url) -- if response.status_code != 200: -- LOGGER.logger.warning(response.content.decode("utf-8")) -- continue -- issue_content_list.extend( -- self.parse_issues_content(response.json())) -- except (JSONDecodeError, Error) as error: -- LOGGER.logger.error(error) -- return issue_content_list -- -- def _save_issues(self, issue_list): -- """ -- Save the obtained issue information -- -- """ -- try: -- issue_ids = [issue['issue_id'] for issue in issue_list] -- with DBHelper(db_name="lifecycle") as database: -- -- @retry(stop_max_attempt_number=3, stop_max_delay=500) -- def _query_pkgissues(): -- exist_issues = database.session.query(PackagesIssue).filter( -- PackagesIssue.issue_id.in_(issue_ids)).all() # pylint: disable=protected-access -- return exist_issues -- -- exist_issues = _query_pkgissues() -- # Save the issue -- for issue_item in issue_list: -- issue_model = [ -- issue for issue in exist_issues if issue.issue_id == issue_item['issue_id']] -- if issue_model: -- for key, val in issue_item.items(): -- setattr(issue_model[0], key, val) -- self.producer_consumer.put( -- copy.deepcopy(issue_model[0])) -- else: -- self.producer_consumer.put( -- PackagesIssue(**issue_item)) -- -- # The number of various issues in the update package -- self.pkg_info.defect = self.defect -- self.pkg_info.feature = self.feature -- self.pkg_info.cve = self.cve -- self.producer_consumer.put(copy.deepcopy(self.pkg_info)) -- -- except (Error, ContentNoneException, SQLAlchemyError) as error: -- LOGGER.logger.error( -- 'An abnormal error occurred while saving related issues:%s' % error if error else '') -- -- def parse_issues_content(self, sources): -- """ -- Description: Parse the response content and get issue content -- Args:Issue list -- -- Returns:list:issue_id, issue_url, issue_content, issue_status, issue_download -- Raises: -- """ -- result_list = [] -- if isinstance(sources, list): -- for source in sources: -- issue_content = self.parse_issue_content(source) -- if issue_content: -- result_list.append(issue_content) -- else: -- issue_content = self.parse_issue_content(sources) -- if issue_content: -- result_list.append(issue_content) -- return result_list -- -- def parse_issue_content(self, source): -- """ -- Description: Parse the response content and get issue content -- Args: Source of issue content -- -- Returns:list:issue_id, issue_url, issue_content, issue_status, issue_download, issue_status -- issue_type, related_release -- Raises:KeyError -- """ -- try: -- result_dict = {"issue_id": source['number'], "issue_url": source['html_url'], -- "issue_title": source['title'].strip(), -- "issue_content": source['body'].strip(), -- "issue_status": source['state'], "issue_download": "", -- "issue_type": source["issue_type"], -- "pkg_name": self.repo, -- "related_release": source["labels"][0]['name'] if source["labels"] else ''} -- if source["issue_type"] == "缺陷": -- self.defect += 1 -- elif source["issue_type"] == "需求": -- self.feature += 1 -- elif source["issue_type"] == "CVE和安全问题": -- self.cve += 1 -- else: -- pass -- except KeyError as error: -- LOGGER.logger.error(error) -- return None -- return result_dict -- -- def issue_hooks(self, issue_hook_info): -- """ -- Description: Hook data triggered by a new task operation -- Args: -- issue_hook_info: Issue info -- Returns: -- -- Raises: -- -- """ -- if issue_hook_info is None: -- raise ContentNoneException( -- 'The content cannot be empty') -- issue_info_list = [] -- issue_info = issue_hook_info["issue"] -- issue_content = self.parse_issue_content(issue_info) -- if issue_content: -- issue_info_list.append(issue_content) -- if self.feature != 0: -- self.defect, self.feature, self.cve = self.pkg_info.defect, self.pkg_info.feature + \ -- 1, self.pkg_info.cve -- if self.defect != 0: -- self.defect, self.feature, self.cve = self.pkg_info.defect + \ -- 1, self.pkg_info.feature, self.pkg_info.cve -- if self.cve != 0: -- self.defect, self.feature, self.cve = self.pkg_info.defect, self.pkg_info.feature, self.pkg_info.cve + 1 -- self._save_issues(issue_info_list) -+#!/usr/bin/python3 -+""" -+Description:Get issue info from gitee -+Class: Gitee -+""" -+import copy -+from json import JSONDecodeError -+from retrying import retry -+import requests -+from requests.exceptions import HTTPError -+from sqlalchemy.exc import SQLAlchemyError -+from packageship.libs.dbutils import DBHelper -+from packageship.libs.configutils.readconfig import ReadConfig -+from packageship.libs.exception import Error, ContentNoneException -+from packageship.application.models.package import PackagesIssue -+from packageship import system_config -+from packageship.libs.log import Log -+from .concurrent import ProducerConsumer -+ -+LOGGER = Log(__name__) -+ -+ -+class Gitee(): -+ """ -+ gitee version management tool related information acquisition -+ -+ """ -+ -+ def __init__(self, pkg_info, owner, repo, table_name): -+ self.pkg_info = pkg_info -+ self.owner = owner -+ self.repo = repo -+ self._read_config = ReadConfig(system_config.SYS_CONFIG_PATH) -+ self.url = "https://gitee.com/" -+ self.api_url = "https://gitee.com/api/v5/repos" -+ self.pool = None -+ self.issue_id = None -+ self.defect = 0 -+ self.feature = 0 -+ self.cve = 0 -+ self.patch_files_path = self._read_config.get_system( -+ "patch_files_path") -+ self.table_name = table_name -+ self.producer_consumer = ProducerConsumer() -+ -+ def query_issues_info(self, issue_id=""): -+ """ -+ Description: View the issue details of the specified package -+ Args: -+ issue_id: Issue id -+ Returns: -+ issue_content_list: The issue details of the specified package list -+ Raises: -+ -+ """ -+ issue_url = self.api_url + \ -+ "/{}/{}/issues/{}".format(self.owner, self.repo, issue_id) -+ try: -+ response = requests.get( -+ issue_url, params={"state": "all", "per_page": 100}) -+ except Error as error: -+ LOGGER.logger.error(error) -+ return None -+ if response.status_code != 200: -+ return None -+ total_page = 1 if issue_id else int(response.headers['total_page']) -+ total_count = int(response.headers['total_count']) -+ if total_count > 0: -+ issue_list = self._query_per_page_issue_info(total_page, issue_url) -+ if not issue_list: -+ LOGGER.logger.error( -+ "An error occurred while querying {}".format(self.repo)) -+ return None -+ self._save_issues(issue_list) -+ -+ def _query_per_page_issue_info(self, total_page, issue_url): -+ """ -+ Description: View the issue details -+ Args: -+ total_page: total page -+ issue_url: issue url -+ -+ Returns: -+ -+ """ -+ issue_content_list = [] -+ for i in range(1, total_page + 1): -+ -+ @retry(stop_max_attempt_number=3, stop_max_delay=1000) -+ def request_issue(page, issue_url): -+ try: -+ response = requests.get(issue_url, -+ params={"state": "all", "per_page": 100, "page": page}) -+ except HTTPError: -+ raise HTTPError('Network request error') -+ return response -+ -+ try: -+ response = request_issue(i, issue_url) -+ if response.status_code != 200: -+ LOGGER.logger.warning(response.content.decode("utf-8")) -+ continue -+ issue_content_list.extend( -+ self.parse_issues_content(response.json())) -+ except (JSONDecodeError, Error) as error: -+ LOGGER.logger.error(error) -+ return issue_content_list -+ -+ def _save_issues(self, issue_list): -+ """ -+ Save the obtained issue information -+ -+ """ -+ try: -+ def _save(issue_module): -+ with DBHelper(db_name='lifecycle') as database: -+ -+ exist_issues = database.session.query(PackagesIssue).filter( -+ PackagesIssue.issue_id == issue_module['issue_id']).first() -+ if exist_issues: -+ -+ # Save the issue -+ for key, val in issue_module.items(): -+ setattr(exist_issues, key, val) -+ else: -+ exist_issues = PackagesIssue(**issue_module) -+ database.add(exist_issues) -+ -+ def _save_package(package_module): -+ with DBHelper(db_name='lifecycle') as database: -+ database.add(package_module) -+ -+ for issue_item in issue_list: -+ self.producer_consumer.put( -+ (copy.deepcopy(issue_item), _save)) -+ -+ # The number of various issues in the update package -+ self.pkg_info.defect = self.defect -+ self.pkg_info.feature = self.feature -+ self.pkg_info.cve = self.cve -+ self.producer_consumer.put((copy.deepcopy(self.pkg_info), _save_package)) -+ -+ except (Error, ContentNoneException, SQLAlchemyError) as error: -+ LOGGER.logger.error( -+ 'An abnormal error occurred while saving related issues:%s' % error if error else '') -+ -+ def parse_issues_content(self, sources): -+ """ -+ Description: Parse the response content and get issue content -+ Args:Issue list -+ -+ Returns:list:issue_id, issue_url, issue_content, issue_status, issue_download -+ Raises: -+ """ -+ result_list = [] -+ if isinstance(sources, list): -+ for source in sources: -+ issue_content = self.parse_issue_content(source) -+ if issue_content: -+ result_list.append(issue_content) -+ else: -+ issue_content = self.parse_issue_content(sources) -+ if issue_content: -+ result_list.append(issue_content) -+ return result_list -+ -+ def parse_issue_content(self, source): -+ """ -+ Description: Parse the response content and get issue content -+ Args: Source of issue content -+ -+ Returns:list:issue_id, issue_url, issue_content, issue_status, issue_download, issue_status -+ issue_type, related_release -+ Raises:KeyError -+ """ -+ try: -+ result_dict = {"issue_id": source['number'], "issue_url": source['html_url'], -+ "issue_title": source['title'].strip(), -+ "issue_content": source['body'].strip(), -+ "issue_status": source['state'], "issue_download": "", -+ "issue_type": source["issue_type"], -+ "pkg_name": self.repo, -+ "related_release": source["labels"][0]['name'] if source["labels"] else ''} -+ if source["issue_type"] == "缺陷": -+ self.defect += 1 -+ elif source["issue_type"] == "需求": -+ self.feature += 1 -+ elif source["issue_type"] == "CVE和安全问题": -+ self.cve += 1 -+ else: -+ pass -+ except KeyError as error: -+ LOGGER.logger.error(error) -+ return None -+ return result_dict -+ -+ def issue_hooks(self, issue_hook_info): -+ """ -+ Description: Hook data triggered by a new task operation -+ Args: -+ issue_hook_info: Issue info -+ Returns: -+ -+ Raises: -+ -+ """ -+ if issue_hook_info is None: -+ raise ContentNoneException( -+ 'The content cannot be empty') -+ issue_info_list = [] -+ issue_info = issue_hook_info["issue"] -+ issue_content = self.parse_issue_content(issue_info) -+ if issue_content: -+ issue_info_list.append(issue_content) -+ if self.feature != 0: -+ self.defect, self.feature, self.cve = self.pkg_info.defect, self.pkg_info.feature + \ -+ 1, self.pkg_info.cve -+ if self.defect != 0: -+ self.defect, self.feature, self.cve = self.pkg_info.defect + \ -+ 1, self.pkg_info.feature, self.pkg_info.cve -+ if self.cve != 0: -+ self.defect, self.feature, self.cve = self.pkg_info.defect, self.pkg_info.feature, self.pkg_info.cve + 1 -+ self._save_issues(issue_info_list) -diff -Naru a/packageship/application/apps/lifecycle/view.py b/packageship/application/apps/lifecycle/view.py ---- a/packageship/application/apps/lifecycle/view.py 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/application/apps/lifecycle/view.py 2020-09-22 23:52:49.731821183 +0800 -@@ -1,760 +1,760 @@ --#!/usr/bin/python3 --""" --Life cycle related api interface --""" --import io --import json --import math --import os --from concurrent.futures import ThreadPoolExecutor -- --import pandas as pd --import yaml -- --from flask import request --from flask import jsonify, make_response --from flask import current_app --from flask_restful import Resource --from marshmallow import ValidationError -- --from sqlalchemy.exc import DisconnectionError, SQLAlchemyError -- --from packageship import system_config --from packageship.libs.configutils.readconfig import ReadConfig --from packageship.libs.exception import Error --from packageship.application.apps.package.function.constants import ResponseCode --from packageship.libs.dbutils.sqlalchemy_helper import DBHelper --from packageship.application.models.package import PackagesIssue --from packageship.application.models.package import Packages --from packageship.application.models.package import PackagesMaintainer --from packageship.libs.log import Log --from .serialize import IssueDownloadSchema, PackagesDownloadSchema, IssuePageSchema, IssueSchema --from ..package.serialize import DataFormatVerfi, UpdatePackagesSchema --from .function.gitee import Gitee as gitee -- --LOGGER = Log(__name__) -- -- --# pylint: disable = no-self-use -- --class DownloadFile(Resource): -- """ -- Download the content of the issue or the excel file of the package content -- """ -- -- def _download_excel(self, file_type, table_name=None): -- """ -- Download excel file -- """ -- file_name = 'packages.xlsx' -- if file_type == 'packages': -- download_content = self.__get_packages_content(table_name) -- else: -- file_name = 'issues.xlsx' -- download_content = self.__get_issues_content() -- if download_content is None: -- return jsonify( -- ResponseCode.response_json( -- ResponseCode.SERVICE_ERROR)) -- pd_dataframe = self.__to_dataframe(download_content) -- -- _response = self.__bytes_save(pd_dataframe) -- return self.__set_response_header(_response, file_name) -- -- def __bytes_save(self, data_frame): -- """ -- Save the file content in the form of a binary file stream -- """ -- try: -- bytes_io = io.BytesIO() -- writer = pd.ExcelWriter( # pylint: disable=abstract-class-instantiated -- bytes_io, engine='xlsxwriter') -- data_frame.to_excel(writer, sheet_name='Summary', index=False) -- writer.save() -- writer.close() -- bytes_io.seek(0) -- _response = make_response(bytes_io.getvalue()) -- bytes_io.close() -- return _response -- except (IOError, Error) as io_error: -- current_app.logger.error(io_error) -- return make_response() -- -- def __set_response_header(self, response, file_name): -- """ -- Set http response header information -- """ -- response.headers['Content-Type'] = \ -- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -- response.headers["Cache-Control"] = "no-cache" -- response.headers['Content-Disposition'] = 'attachment; filename={file_name}'.format( -- file_name=file_name) -- return response -- -- def __get_packages_content(self, table_name): -- """ -- Get package list information -- """ -- try: -- with DBHelper(db_name='lifecycle') as database: -- # Query all package data in the specified table -- _model = Packages.package_meta(table_name) -- _packageinfos = database.session.query(_model).all() -- packages_dicts = PackagesDownloadSchema( -- many=True).dump(_packageinfos) -- return packages_dicts -- -- except (SQLAlchemyError, DisconnectionError) as error: -- current_app.logger.error(error) -- return None -- -- def __get_issues_content(self): -- """ -- Get the list of issues -- """ -- try: -- with DBHelper(db_name='lifecycle') as database: -- _issues = database.session.query(PackagesIssue).all() -- issues_dicts = IssueDownloadSchema(many=True).dump(_issues) -- return issues_dicts -- except (SQLAlchemyError, DisconnectionError) as error: -- current_app.logger.error(error) -- return None -- -- def __to_dataframe(self, datas): -- """ -- Convert the obtained information into pandas content format -- """ -- -- data_frame = pd.DataFrame(datas) -- return data_frame -- -- def get(self, file_type): -- """ -- Download package collection information and isse list information -- -- """ -- if file_type not in ['packages', 'issues']: -- return jsonify( -- ResponseCode.response_json( -- ResponseCode.PARAM_ERROR)) -- -- table_name = request.args.get('table_name', None) -- response = self._download_excel(file_type, table_name) -- return response -- -- --class MaintainerView(Resource): -- """ -- Maintainer name collection -- """ -- -- def __query_maintainers(self): -- """ -- Query the names of all maintainers in the specified table -- """ -- try: -- with DBHelper(db_name='lifecycle') as database: -- maintainers = database.session.query( -- PackagesMaintainer.maintainer).group_by(PackagesMaintainer.maintainer).all() -- return [maintainer_item[0] for maintainer_item in maintainers -- if maintainer_item[0]] -- except (SQLAlchemyError, DisconnectionError) as error: -- current_app.logger.error(error) -- return [] -- -- def get(self): -- """ -- Get the list of maintainers -- """ -- # Group query of the names of all maintainers in the current table -- maintainers = self.__query_maintainers() -- return jsonify(ResponseCode.response_json( -- ResponseCode.SUCCESS, -- maintainers)) -- -- --class TableColView(Resource): -- """ -- The default column of the package shows the interface -- """ -- -- def __columns_names(self): -- """ -- Mapping of column name and title -- """ -- columns = [ -- ('name', 'Name', True), -- ('version', 'Version', True), -- ('release', 'Release', True), -- ('url', 'Url', True), -- ('rpm_license', 'License', False), -- ('feature', 'Feature', False), -- ('maintainer', 'Maintainer', True), -- ('maintainlevel', 'Maintenance Level', True), -- ('release_time', 'Release Time', False), -- ('used_time', 'Used Time', True), -- ('maintainer_status', 'Maintain Status', True), -- ('latest_version', 'Latest Version', False), -- ('latest_version_time', 'Latest Version Release Time', False), -- ('issue', 'Issue', True)] -- return columns -- -- def __columns_mapping(self): -- """ -- -- """ -- columns = list() -- for column in self.__columns_names(): -- columns.append({ -- 'column_name': column[0], -- 'label': column[1], -- 'default_selected': column[2] -- }) -- return columns -- -- def get(self): -- """ -- Get the default display column of the package -- -- """ -- table_mapping_columns = self.__columns_mapping() -- return jsonify( -- ResponseCode.response_json( -- ResponseCode.SUCCESS, -- table_mapping_columns)) -- -- --class LifeTables(Resource): -- """ -- description: LifeTables -- Restful API: get -- ChangeLog: -- """ -- -- def get(self): -- """ -- return all table names in the database -- -- Returns: -- Return the table names in the database as a list -- """ -- try: -- with DBHelper(db_name="lifecycle") as database_name: -- # View all table names in the package-info database -- all_table_names = database_name.engine.table_names() -- all_table_names.remove("packages_issue") -- all_table_names.remove("packages_maintainer") -- return jsonify( -- ResponseCode.response_json( -- ResponseCode.SUCCESS, data=all_table_names) -- ) -- except (SQLAlchemyError, DisconnectionError, Error, ValueError) as sql_error: -- LOGGER.logger.error(sql_error) -- return jsonify( -- ResponseCode.response_json(ResponseCode.DATABASE_NOT_FOUND) -- ) -- -- --class IssueView(Resource): -- """ -- Issue content collection -- """ -- -- def _query_issues(self, request_data): -- """ -- Args: -- request_data: -- Returns: -- """ -- try: -- with DBHelper(db_name='lifecycle') as database: -- issues_query = database.session.query(PackagesIssue.issue_id, -- PackagesIssue.issue_url, -- PackagesIssue.issue_title, -- PackagesIssue.issue_status, -- PackagesIssue.pkg_name, -- PackagesIssue.issue_type, -- PackagesMaintainer.maintainer). \ -- outerjoin(PackagesMaintainer, -- PackagesMaintainer.name == PackagesIssue.pkg_name) -- if request_data.get("pkg_name"): -- issues_query = issues_query.filter( -- PackagesIssue.pkg_name == request_data.get("pkg_name")) -- if request_data.get("issue_type"): -- issues_query = issues_query.filter( -- PackagesIssue.issue_type == request_data.get("issue_type")) -- if request_data.get("issue_status"): -- issues_query = issues_query.filter( -- PackagesIssue.issue_status == request_data.get("issue_status")) -- if request_data.get("maintainer"): -- issues_query = issues_query.filter( -- PackagesMaintainer.maintainer == request_data.get("maintainer")) -- total_count = issues_query.count() -- total_page = math.ceil( -- total_count / int(request_data.get("page_size"))) -- issues_query = issues_query.limit(request_data.get("page_size")).offset( -- (int(request_data.get("page_num")) - 1) * int(request_data.get("page_size"))) -- issue_dicts = IssuePageSchema( -- many=True).dump(issues_query.all()) -- issue_data = ResponseCode.response_json( -- ResponseCode.SUCCESS, issue_dicts) -- issue_data['total_count'] = total_count -- issue_data['total_page'] = total_page -- return issue_data -- except (SQLAlchemyError, DisconnectionError) as error: -- current_app.logger.error(error) -- return ResponseCode.response_json(ResponseCode.DATABASE_NOT_FOUND) -- -- def get(self): -- """ -- Description: Get all issues info or one specific issue -- Args: -- Returns: -- [ -- { -- "issue_id": "", -- "issue_url": "", -- "issue_title": "", -- "issue_content": "", -- "issue_status": "", -- "issue_type": "" -- }, -- ] -- Raises: -- DisconnectionError: Unable to connect to database exception -- AttributeError: Object does not have this property -- TypeError: Exception of type -- Error: Abnormal error -- """ -- schema = IssueSchema() -- if schema.validate(request.args): -- return jsonify( -- ResponseCode.response_json(ResponseCode.PARAM_ERROR) -- ) -- issue_dict = self._query_issues(request.args) -- return issue_dict -- -- --class IssueType(Resource): -- """ -- Issue type collection -- """ -- -- def _get_issue_type(self): -- """ -- Description: Query issue type -- Returns: -- """ -- try: -- with DBHelper(db_name='lifecycle') as database: -- issues_query = database.session.query(PackagesIssue.issue_type).group_by( -- PackagesIssue.issue_type).all() -- return jsonify(ResponseCode.response_json( -- ResponseCode.SUCCESS, [issue_query[0] for issue_query in issues_query])) -- except (SQLAlchemyError, DisconnectionError) as error: -- current_app.logger.error(error) -- return jsonify(ResponseCode.response_json( -- ResponseCode.PARAM_ERROR)) -- -- def get(self): -- """ -- Description: Get all issues info or one specific issue -- Args: -- Returns: -- [ -- "issue_type", -- "issue_type" -- ] -- Raises: -- DisconnectionError: Unable to connect to database exception -- AttributeError: Object does not have this property -- TypeError: Exception of type -- Error: Abnormal error -- """ -- return self._get_issue_type() -- -- --class IssueStatus(Resource): -- """ -- Issue status collection -- """ -- -- def _get_issue_status(self): -- """ -- Description: Query issue status -- Returns: -- """ -- try: -- with DBHelper(db_name='lifecycle') as database: -- issues_query = database.session.query(PackagesIssue.issue_status).group_by( -- PackagesIssue.issue_status).all() -- return jsonify(ResponseCode.response_json( -- ResponseCode.SUCCESS, [issue_query[0] for issue_query in issues_query])) -- except (SQLAlchemyError, DisconnectionError) as error: -- current_app.logger.error(error) -- return jsonify(ResponseCode.response_json( -- ResponseCode.PARAM_ERROR)) -- -- def get(self): -- """ -- Description: Get all issues info or one specific issue -- Args: -- Returns: -- [ -- "issue_status", -- "issue_status" -- ] -- Raises: -- DisconnectionError: Unable to connect to database exception -- AttributeError: Object does not have this property -- TypeError: Exception of type -- Error: Abnormal error -- """ -- return self._get_issue_status() -- -- --class IssueCatch(Resource): -- """ -- description: Catch issue content -- Restful API: put -- ChangeLog: -- """ -- -- def post(self): -- """ -- Searching issue content -- Args: -- Returns: -- for examples: -- [ -- { -- "issue_id": "", -- "issue_url": "", -- "issue_title": "", -- "issue_content": "", -- "issue_status": "", -- "issue_type": "" -- }, -- ] -- Raises: -- DisconnectionError: Unable to connect to database exception -- AttributeError: Object does not have this property -- TypeError: Exception of type -- Error: Abnormal error -- """ -- data = json.loads(request.get_data()) -- if not isinstance(data, dict): -- return jsonify( -- ResponseCode.response_json(ResponseCode.PARAM_ERROR)) -- pkg_name = data["repository"]["path"] -- try: -- _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -- pool_workers = _readconfig.get_config('LIFECYCLE', 'pool_workers') -- _warehouse = _readconfig.get_config('LIFECYCLE', 'warehouse') -- if _warehouse is None: -- _warehouse = 'src-openeuler' -- if not isinstance(pool_workers, int): -- pool_workers = 10 -- pool = ThreadPoolExecutor(max_workers=pool_workers) -- with DBHelper(db_name="lifecycle") as database: -- for table_name in filter(lambda x: x not in ['packages_issue', 'packages_maintainer'], -- database.engine.table_names()): -- cls_model = Packages.package_meta(table_name) -- for package_item in database.session.query(cls_model).filter( -- cls_model.name == pkg_name).all(): -- gitee_issue = gitee( -- package_item, _warehouse, package_item.name, table_name) -- pool.submit(gitee_issue.issue_hooks, data) -- pool.shutdown() -- return jsonify(ResponseCode.response_json(ResponseCode.SUCCESS)) -- except SQLAlchemyError as error_msg: -- current_app.logger.error(error_msg) -- -- --class UpdatePackages(Resource): -- """ -- description:Life cycle update information of a single package -- Restful API: post -- ChangeLog: -- """ -- -- def _get_all_yaml_name(self, filepath): -- """ -- List of all yaml file names in the folder -- -- Args: -- filepath: file path -- -- Returns: -- yaml_file_list:List of all yaml file names in the folder -- -- Attributes: -- Error:Error -- NotADirectoryError:Invalid directory name -- FileNotFoundError:File not found error -- -- """ -- try: -- yaml_file_list = os.listdir(filepath) -- return yaml_file_list -- except (Error, NotADirectoryError, FileNotFoundError) as error: -- current_app.logger.error(error) -- return None -- -- def _get_yaml_content(self, yaml_file, filepath): -- """ -- Read the content of the yaml file -- -- Args: -- yaml_file: yaml file -- filepath: file path -- -- Returns: -- Return a dictionary containing name, maintainer and maintainlevel -- """ -- yaml_data_dict = dict() -- if not yaml_file.endswith(".yaml"): -- return None -- pkg_name = yaml_file.rsplit('.yaml')[0] -- single_yaml_path = os.path.join(filepath, yaml_file) -- with open(single_yaml_path, 'r', encoding='utf-8') as file_context: -- yaml_flie_data = yaml.load( -- file_context.read(), Loader=yaml.FullLoader) -- if yaml_flie_data is None or not isinstance(yaml_flie_data, dict): -- return None -- maintainer = yaml_flie_data.get("maintainer") -- maintainlevel = yaml_flie_data.get("maintainlevel") -- yaml_data_dict['name'] = pkg_name -- if maintainer: -- yaml_data_dict['maintainer'] = maintainer -- if maintainlevel: -- yaml_data_dict['maintainlevel'] = maintainlevel -- return yaml_data_dict -- -- def _read_yaml_file(self, filepath): -- """ -- Read the yaml file and combine the data of the nested dictionary of the list -- -- Args: -- filepath: file path -- -- Returns: -- yaml.YAMLError:yaml file error -- SQLAlchemyError:SQLAlchemy Error -- DisconnectionError:Connect to database error -- Error:Error -- """ -- yaml_file_list = self._get_all_yaml_name(filepath) -- if not yaml_file_list: -- return None -- try: -- yaml_data_list = list() -- _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -- pool_workers = _readconfig.get_config('LIFECYCLE', 'pool_workers') -- if not isinstance(pool_workers, int): -- pool_workers = 10 -- with ThreadPoolExecutor(max_workers=pool_workers) as pool: -- for yaml_file in yaml_file_list: -- pool_result = pool.submit( -- self._get_yaml_content, yaml_file, filepath) -- yaml_data_dict = pool_result.result() -- yaml_data_list.append(yaml_data_dict) -- return yaml_data_list -- except (yaml.YAMLError, SQLAlchemyError, DisconnectionError, Error) as error: -- current_app.logger.error(error) -- return None -- -- def _verification_yaml_data_list(self, yaml_data_list): -- """ -- Verify the data obtained in the yaml file -- -- Args: -- yaml_data_list: yaml data list -- -- Returns: -- yaml_data_list: After verification yaml data list -- -- Attributes: -- ValidationError: Validation error -- -- """ -- try: -- DataFormatVerfi(many=True).load(yaml_data_list) -- return yaml_data_list -- except ValidationError as error: -- current_app.logger.error(error.messages) -- return None -- -- def _save_in_database(self, yaml_data_list): -- """ -- Save the data to the database -- -- Args: -- tbname: Table Name -- name_separate_list: Split name list -- _update_pack_data: Split new list of combined data -- -- Returns: -- SUCCESS or UPDATA_DATA_FAILED -- -- Attributes -- DisconnectionError: Connect to database error -- SQLAlchemyError: SQLAlchemy Error -- Error: Error -- -- """ -- try: -- with DBHelper(db_name="lifecycle") as database_name: -- if 'packages_maintainer' not in database_name.engine.table_names(): -- return jsonify(ResponseCode.response_json( -- ResponseCode.TABLE_NAME_NOT_EXIST)) -- database_name.session.begin(subtransactions=True) -- for yaml_data in yaml_data_list: -- name = yaml_data.get("name") -- maintainer = yaml_data.get("maintainer") -- maintainlevel = yaml_data.get("maintainlevel") -- packages_maintainer_obj = database_name.session.query( -- PackagesMaintainer).filter_by(name=name).first() -- if packages_maintainer_obj: -- if maintainer: -- packages_maintainer_obj.maintainer = maintainer -- if maintainlevel: -- packages_maintainer_obj.maintainlevel = maintainlevel -- else: -- database_name.add(PackagesMaintainer( -- name=name, maintainer=maintainer, maintainlevel=maintainlevel -- )) -- database_name.session.commit() -- return jsonify(ResponseCode.response_json( -- ResponseCode.SUCCESS)) -- except (DisconnectionError, SQLAlchemyError, Error, AttributeError) as error: -- current_app.logger.error(error) -- return jsonify(ResponseCode.response_json( -- ResponseCode.UPDATA_DATA_FAILED)) -- -- def _overall_process( -- self, -- filepath): -- """ -- Call each method to complete the entire function -- -- Args: -- filepath: file path -- tbname: table name -- -- Returns: -- SUCCESS or UPDATA_DATA_FAILED -- -- Attributes -- DisconnectionError: Connect to database error -- SQLAlchemyError: SQLAlchemy Error -- Error: Error -- """ -- try: -- if filepath is None or not os.path.exists(filepath): -- return jsonify(ResponseCode.response_json( -- ResponseCode.SPECIFIED_FILE_NOT_EXIST)) -- yaml_file_list = self._get_all_yaml_name(filepath) -- if not yaml_file_list: -- return jsonify(ResponseCode.response_json( -- ResponseCode.EMPTY_FOLDER)) -- yaml_data_list_result = self._read_yaml_file(filepath) -- yaml_data_list = self._verification_yaml_data_list( -- yaml_data_list_result) -- if yaml_data_list is None: -- return jsonify(ResponseCode.response_json( -- ResponseCode.YAML_FILE_ERROR)) -- result = self._save_in_database( -- yaml_data_list) -- return result -- except (DisconnectionError, SQLAlchemyError, Error) as error: -- current_app.logger.error(error) -- return jsonify(ResponseCode.response_json( -- ResponseCode.UPDATA_DATA_FAILED)) -- -- def _update_single_package_info( -- self, srcname, maintainer, maintainlevel): -- """ -- Update the maintainer field and maintainlevel -- field of a single package -- -- Args: -- srcname: The name of the source package -- maintainer: Package maintainer -- maintainlevel: Package maintenance level -- -- Returns: -- success or failed -- -- Attributes -- SQLAlchemyError: sqlalchemy error -- DisconnectionError: Cannot connect to database error -- Error: Error -- """ -- if not srcname: -- return jsonify( -- ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) -- ) -- if not maintainer and not maintainlevel: -- return jsonify( -- ResponseCode.response_json(ResponseCode.PARAM_ERROR) -- ) -- try: -- with DBHelper(db_name='lifecycle') as database_name: -- if 'packages_maintainer' not in database_name.engine.table_names(): -- return jsonify(ResponseCode.response_json( -- ResponseCode.TABLE_NAME_NOT_EXIST)) -- update_obj = database_name.session.query( -- PackagesMaintainer).filter_by(name=srcname).first() -- if update_obj: -- if maintainer: -- update_obj.maintainer = maintainer -- if maintainlevel: -- update_obj.maintainlevel = maintainlevel -- else: -- database_name.add(PackagesMaintainer( -- name=srcname, maintainer=maintainer, maintainlevel=maintainlevel -- )) -- database_name.session.commit() -- return jsonify( -- ResponseCode.response_json( -- ResponseCode.SUCCESS)) -- except (SQLAlchemyError, DisconnectionError, Error) as sql_error: -- current_app.logger.error(sql_error) -- database_name.session.rollback() -- return jsonify(ResponseCode.response_json( -- ResponseCode.UPDATA_DATA_FAILED -- )) -- -- def put(self): -- """ -- Life cycle update information of a single package or -- All packages -- -- Returns: -- for example:: -- { -- "code": "", -- "data": "", -- "msg": "" -- } -- """ -- schema = UpdatePackagesSchema() -- data = request.get_json() -- if schema.validate(data): -- return jsonify( -- ResponseCode.response_json(ResponseCode.PARAM_ERROR) -- ) -- srcname = data.get('pkg_name', None) -- maintainer = data.get('maintainer', None) -- maintainlevel = data.get('maintainlevel', None) -- batch = data.get('batch') -- filepath = data.get('filepath', None) -- -- if batch: -- result = self._overall_process(filepath) -- else: -- result = self._update_single_package_info( -- srcname, maintainer, maintainlevel) -- return result -+#!/usr/bin/python3 -+""" -+Life cycle related api interface -+""" -+import io -+import json -+import math -+import os -+from concurrent.futures import ThreadPoolExecutor -+ -+import pandas as pd -+import yaml -+ -+from flask import request -+from flask import jsonify, make_response -+from flask import current_app -+from flask_restful import Resource -+from marshmallow import ValidationError -+ -+from sqlalchemy.exc import DisconnectionError, SQLAlchemyError -+ -+from packageship import system_config -+from packageship.libs.configutils.readconfig import ReadConfig -+from packageship.libs.exception import Error -+from packageship.application.apps.package.function.constants import ResponseCode -+from packageship.libs.dbutils.sqlalchemy_helper import DBHelper -+from packageship.application.models.package import PackagesIssue -+from packageship.application.models.package import Packages -+from packageship.application.models.package import PackagesMaintainer -+from packageship.libs.log import Log -+from .serialize import IssueDownloadSchema, PackagesDownloadSchema, IssuePageSchema, IssueSchema -+from ..package.serialize import DataFormatVerfi, UpdatePackagesSchema -+from .function.gitee import Gitee as gitee -+ -+LOGGER = Log(__name__) -+ -+ -+# pylint: disable = no-self-use -+ -+class DownloadFile(Resource): -+ """ -+ Download the content of the issue or the excel file of the package content -+ """ -+ -+ def _download_excel(self, file_type, table_name=None): -+ """ -+ Download excel file -+ """ -+ file_name = 'packages.xlsx' -+ if file_type == 'packages': -+ download_content = self.__get_packages_content(table_name) -+ else: -+ file_name = 'issues.xlsx' -+ download_content = self.__get_issues_content() -+ if download_content is None: -+ return jsonify( -+ ResponseCode.response_json( -+ ResponseCode.SERVICE_ERROR)) -+ pd_dataframe = self.__to_dataframe(download_content) -+ -+ _response = self.__bytes_save(pd_dataframe) -+ return self.__set_response_header(_response, file_name) -+ -+ def __bytes_save(self, data_frame): -+ """ -+ Save the file content in the form of a binary file stream -+ """ -+ try: -+ bytes_io = io.BytesIO() -+ writer = pd.ExcelWriter( # pylint: disable=abstract-class-instantiated -+ bytes_io, engine='xlsxwriter') -+ data_frame.to_excel(writer, sheet_name='Summary', index=False) -+ writer.save() -+ writer.close() -+ bytes_io.seek(0) -+ _response = make_response(bytes_io.getvalue()) -+ bytes_io.close() -+ return _response -+ except (IOError, Error) as io_error: -+ current_app.logger.error(io_error) -+ return make_response() -+ -+ def __set_response_header(self, response, file_name): -+ """ -+ Set http response header information -+ """ -+ response.headers['Content-Type'] = \ -+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -+ response.headers["Cache-Control"] = "no-cache" -+ response.headers['Content-Disposition'] = 'attachment; filename={file_name}'.format( -+ file_name=file_name) -+ return response -+ -+ def __get_packages_content(self, table_name): -+ """ -+ Get package list information -+ """ -+ try: -+ with DBHelper(db_name='lifecycle') as database: -+ # Query all package data in the specified table -+ _model = Packages.package_meta(table_name) -+ _packageinfos = database.session.query(_model).all() -+ packages_dicts = PackagesDownloadSchema( -+ many=True).dump(_packageinfos) -+ return packages_dicts -+ -+ except (SQLAlchemyError, DisconnectionError) as error: -+ current_app.logger.error(error) -+ return None -+ -+ def __get_issues_content(self): -+ """ -+ Get the list of issues -+ """ -+ try: -+ with DBHelper(db_name='lifecycle') as database: -+ _issues = database.session.query(PackagesIssue).all() -+ issues_dicts = IssueDownloadSchema(many=True).dump(_issues) -+ return issues_dicts -+ except (SQLAlchemyError, DisconnectionError) as error: -+ current_app.logger.error(error) -+ return None -+ -+ def __to_dataframe(self, datas): -+ """ -+ Convert the obtained information into pandas content format -+ """ -+ -+ data_frame = pd.DataFrame(datas) -+ return data_frame -+ -+ def get(self, file_type): -+ """ -+ Download package collection information and isse list information -+ -+ """ -+ if file_type not in ['packages', 'issues']: -+ return jsonify( -+ ResponseCode.response_json( -+ ResponseCode.PARAM_ERROR)) -+ -+ table_name = request.args.get('table_name', None) -+ response = self._download_excel(file_type, table_name) -+ return response -+ -+ -+class MaintainerView(Resource): -+ """ -+ Maintainer name collection -+ """ -+ -+ def __query_maintainers(self): -+ """ -+ Query the names of all maintainers in the specified table -+ """ -+ try: -+ with DBHelper(db_name='lifecycle') as database: -+ maintainers = database.session.query( -+ PackagesMaintainer.maintainer).group_by(PackagesMaintainer.maintainer).all() -+ return [maintainer_item[0] for maintainer_item in maintainers -+ if maintainer_item[0]] -+ except (SQLAlchemyError, DisconnectionError) as error: -+ current_app.logger.error(error) -+ return [] -+ -+ def get(self): -+ """ -+ Get the list of maintainers -+ """ -+ # Group query of the names of all maintainers in the current table -+ maintainers = self.__query_maintainers() -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.SUCCESS, -+ maintainers)) -+ -+ -+class TableColView(Resource): -+ """ -+ The default column of the package shows the interface -+ """ -+ -+ def __columns_names(self): -+ """ -+ Mapping of column name and title -+ """ -+ columns = [ -+ ('name', 'Name', True), -+ ('version', 'Version', True), -+ ('release', 'Release', True), -+ ('url', 'Url', True), -+ ('rpm_license', 'License', False), -+ ('feature', 'Feature', False), -+ ('maintainer', 'Maintainer', True), -+ ('maintainlevel', 'Maintenance Level', True), -+ ('release_time', 'Release Time', False), -+ ('used_time', 'Used Time', True), -+ ('maintainer_status', 'Maintain Status', True), -+ ('latest_version', 'Latest Version', False), -+ ('latest_version_time', 'Latest Version Release Time', False), -+ ('issue', 'Issue', True)] -+ return columns -+ -+ def __columns_mapping(self): -+ """ -+ -+ """ -+ columns = list() -+ for column in self.__columns_names(): -+ columns.append({ -+ 'column_name': column[0], -+ 'label': column[1], -+ 'default_selected': column[2] -+ }) -+ return columns -+ -+ def get(self): -+ """ -+ Get the default display column of the package -+ -+ """ -+ table_mapping_columns = self.__columns_mapping() -+ return jsonify( -+ ResponseCode.response_json( -+ ResponseCode.SUCCESS, -+ table_mapping_columns)) -+ -+ -+class LifeTables(Resource): -+ """ -+ description: LifeTables -+ Restful API: get -+ ChangeLog: -+ """ -+ -+ def get(self): -+ """ -+ return all table names in the database -+ -+ Returns: -+ Return the table names in the database as a list -+ """ -+ try: -+ with DBHelper(db_name="lifecycle") as database_name: -+ # View all table names in the package-info database -+ all_table_names = database_name.engine.table_names() -+ all_table_names.remove("packages_issue") -+ all_table_names.remove("packages_maintainer") -+ return jsonify( -+ ResponseCode.response_json( -+ ResponseCode.SUCCESS, data=all_table_names) -+ ) -+ except (SQLAlchemyError, DisconnectionError, Error, ValueError) as sql_error: -+ LOGGER.logger.error(sql_error) -+ return jsonify( -+ ResponseCode.response_json(ResponseCode.DATABASE_NOT_FOUND) -+ ) -+ -+ -+class IssueView(Resource): -+ """ -+ Issue content collection -+ """ -+ -+ def _query_issues(self, request_data): -+ """ -+ Args: -+ request_data: -+ Returns: -+ """ -+ try: -+ with DBHelper(db_name='lifecycle') as database: -+ issues_query = database.session.query(PackagesIssue.issue_id, -+ PackagesIssue.issue_url, -+ PackagesIssue.issue_title, -+ PackagesIssue.issue_status, -+ PackagesIssue.pkg_name, -+ PackagesIssue.issue_type, -+ PackagesMaintainer.maintainer). \ -+ outerjoin(PackagesMaintainer, -+ PackagesMaintainer.name == PackagesIssue.pkg_name) -+ if request_data.get("pkg_name"): -+ issues_query = issues_query.filter( -+ PackagesIssue.pkg_name == request_data.get("pkg_name")) -+ if request_data.get("issue_type"): -+ issues_query = issues_query.filter( -+ PackagesIssue.issue_type == request_data.get("issue_type")) -+ if request_data.get("issue_status"): -+ issues_query = issues_query.filter( -+ PackagesIssue.issue_status == request_data.get("issue_status")) -+ if request_data.get("maintainer"): -+ issues_query = issues_query.filter( -+ PackagesMaintainer.maintainer == request_data.get("maintainer")) -+ total_count = issues_query.count() -+ total_page = math.ceil( -+ total_count / int(request_data.get("page_size"))) -+ issues_query = issues_query.limit(request_data.get("page_size")).offset( -+ (int(request_data.get("page_num")) - 1) * int(request_data.get("page_size"))) -+ issue_dicts = IssuePageSchema( -+ many=True).dump(issues_query.all()) -+ issue_data = ResponseCode.response_json( -+ ResponseCode.SUCCESS, issue_dicts) -+ issue_data['total_count'] = total_count -+ issue_data['total_page'] = total_page -+ return issue_data -+ except (SQLAlchemyError, DisconnectionError) as error: -+ current_app.logger.error(error) -+ return ResponseCode.response_json(ResponseCode.DATABASE_NOT_FOUND) -+ -+ def get(self): -+ """ -+ Description: Get all issues info or one specific issue -+ Args: -+ Returns: -+ [ -+ { -+ "issue_id": "", -+ "issue_url": "", -+ "issue_title": "", -+ "issue_content": "", -+ "issue_status": "", -+ "issue_type": "" -+ }, -+ ] -+ Raises: -+ DisconnectionError: Unable to connect to database exception -+ AttributeError: Object does not have this property -+ TypeError: Exception of type -+ Error: Abnormal error -+ """ -+ schema = IssueSchema() -+ if schema.validate(request.args): -+ return jsonify( -+ ResponseCode.response_json(ResponseCode.PARAM_ERROR) -+ ) -+ issue_dict = self._query_issues(request.args) -+ return issue_dict -+ -+ -+class IssueType(Resource): -+ """ -+ Issue type collection -+ """ -+ -+ def _get_issue_type(self): -+ """ -+ Description: Query issue type -+ Returns: -+ """ -+ try: -+ with DBHelper(db_name='lifecycle') as database: -+ issues_query = database.session.query(PackagesIssue.issue_type).group_by( -+ PackagesIssue.issue_type).all() -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.SUCCESS, [issue_query[0] for issue_query in issues_query])) -+ except (SQLAlchemyError, DisconnectionError) as error: -+ current_app.logger.error(error) -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.PARAM_ERROR)) -+ -+ def get(self): -+ """ -+ Description: Get all issues info or one specific issue -+ Args: -+ Returns: -+ [ -+ "issue_type", -+ "issue_type" -+ ] -+ Raises: -+ DisconnectionError: Unable to connect to database exception -+ AttributeError: Object does not have this property -+ TypeError: Exception of type -+ Error: Abnormal error -+ """ -+ return self._get_issue_type() -+ -+ -+class IssueStatus(Resource): -+ """ -+ Issue status collection -+ """ -+ -+ def _get_issue_status(self): -+ """ -+ Description: Query issue status -+ Returns: -+ """ -+ try: -+ with DBHelper(db_name='lifecycle') as database: -+ issues_query = database.session.query(PackagesIssue.issue_status).group_by( -+ PackagesIssue.issue_status).all() -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.SUCCESS, [issue_query[0] for issue_query in issues_query])) -+ except (SQLAlchemyError, DisconnectionError) as error: -+ current_app.logger.error(error) -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.PARAM_ERROR)) -+ -+ def get(self): -+ """ -+ Description: Get all issues info or one specific issue -+ Args: -+ Returns: -+ [ -+ "issue_status", -+ "issue_status" -+ ] -+ Raises: -+ DisconnectionError: Unable to connect to database exception -+ AttributeError: Object does not have this property -+ TypeError: Exception of type -+ Error: Abnormal error -+ """ -+ return self._get_issue_status() -+ -+ -+class IssueCatch(Resource): -+ """ -+ description: Catch issue content -+ Restful API: put -+ ChangeLog: -+ """ -+ -+ def post(self): -+ """ -+ Searching issue content -+ Args: -+ Returns: -+ for examples: -+ [ -+ { -+ "issue_id": "", -+ "issue_url": "", -+ "issue_title": "", -+ "issue_content": "", -+ "issue_status": "", -+ "issue_type": "" -+ }, -+ ] -+ Raises: -+ DisconnectionError: Unable to connect to database exception -+ AttributeError: Object does not have this property -+ TypeError: Exception of type -+ Error: Abnormal error -+ """ -+ data = json.loads(request.get_data()) -+ if not isinstance(data, dict): -+ return jsonify( -+ ResponseCode.response_json(ResponseCode.PARAM_ERROR)) -+ pkg_name = data["repository"]["path"] -+ try: -+ _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -+ pool_workers = _readconfig.get_config('LIFECYCLE', 'pool_workers') -+ _warehouse = _readconfig.get_config('LIFECYCLE', 'warehouse') -+ if _warehouse is None: -+ _warehouse = 'src-openeuler' -+ if not isinstance(pool_workers, int): -+ pool_workers = 10 -+ pool = ThreadPoolExecutor(max_workers=pool_workers) -+ with DBHelper(db_name="lifecycle") as database: -+ for table_name in filter(lambda x: x not in ['packages_issue', 'packages_maintainer', 'database_info'], -+ database.engine.table_names()): -+ cls_model = Packages.package_meta(table_name) -+ for package_item in database.session.query(cls_model).filter( -+ cls_model.name == pkg_name).all(): -+ gitee_issue = gitee( -+ package_item, _warehouse, package_item.name, table_name) -+ pool.submit(gitee_issue.issue_hooks, data) -+ pool.shutdown() -+ return jsonify(ResponseCode.response_json(ResponseCode.SUCCESS)) -+ except SQLAlchemyError as error_msg: -+ current_app.logger.error(error_msg) -+ -+ -+class UpdatePackages(Resource): -+ """ -+ description:Life cycle update information of a single package -+ Restful API: post -+ ChangeLog: -+ """ -+ -+ def _get_all_yaml_name(self, filepath): -+ """ -+ List of all yaml file names in the folder -+ -+ Args: -+ filepath: file path -+ -+ Returns: -+ yaml_file_list:List of all yaml file names in the folder -+ -+ Attributes: -+ Error:Error -+ NotADirectoryError:Invalid directory name -+ FileNotFoundError:File not found error -+ -+ """ -+ try: -+ yaml_file_list = os.listdir(filepath) -+ return yaml_file_list -+ except (Error, NotADirectoryError, FileNotFoundError) as error: -+ current_app.logger.error(error) -+ return None -+ -+ def _get_yaml_content(self, yaml_file, filepath): -+ """ -+ Read the content of the yaml file -+ -+ Args: -+ yaml_file: yaml file -+ filepath: file path -+ -+ Returns: -+ Return a dictionary containing name, maintainer and maintainlevel -+ """ -+ yaml_data_dict = dict() -+ if not yaml_file.endswith(".yaml"): -+ return None -+ pkg_name = yaml_file.rsplit('.yaml')[0] -+ single_yaml_path = os.path.join(filepath, yaml_file) -+ with open(single_yaml_path, 'r', encoding='utf-8') as file_context: -+ yaml_flie_data = yaml.load( -+ file_context.read(), Loader=yaml.FullLoader) -+ if yaml_flie_data is None or not isinstance(yaml_flie_data, dict): -+ return None -+ maintainer = yaml_flie_data.get("maintainer") -+ maintainlevel = yaml_flie_data.get("maintainlevel") -+ yaml_data_dict['name'] = pkg_name -+ if maintainer: -+ yaml_data_dict['maintainer'] = maintainer -+ if maintainlevel: -+ yaml_data_dict['maintainlevel'] = maintainlevel -+ return yaml_data_dict -+ -+ def _read_yaml_file(self, filepath): -+ """ -+ Read the yaml file and combine the data of the nested dictionary of the list -+ -+ Args: -+ filepath: file path -+ -+ Returns: -+ yaml.YAMLError:yaml file error -+ SQLAlchemyError:SQLAlchemy Error -+ DisconnectionError:Connect to database error -+ Error:Error -+ """ -+ yaml_file_list = self._get_all_yaml_name(filepath) -+ if not yaml_file_list: -+ return None -+ try: -+ yaml_data_list = list() -+ _readconfig = ReadConfig(system_config.SYS_CONFIG_PATH) -+ pool_workers = _readconfig.get_config('LIFECYCLE', 'pool_workers') -+ if not isinstance(pool_workers, int): -+ pool_workers = 10 -+ with ThreadPoolExecutor(max_workers=pool_workers) as pool: -+ for yaml_file in yaml_file_list: -+ pool_result = pool.submit( -+ self._get_yaml_content, yaml_file, filepath) -+ yaml_data_dict = pool_result.result() -+ yaml_data_list.append(yaml_data_dict) -+ return yaml_data_list -+ except (yaml.YAMLError, SQLAlchemyError, DisconnectionError, Error) as error: -+ current_app.logger.error(error) -+ return None -+ -+ def _verification_yaml_data_list(self, yaml_data_list): -+ """ -+ Verify the data obtained in the yaml file -+ -+ Args: -+ yaml_data_list: yaml data list -+ -+ Returns: -+ yaml_data_list: After verification yaml data list -+ -+ Attributes: -+ ValidationError: Validation error -+ -+ """ -+ try: -+ DataFormatVerfi(many=True).load(yaml_data_list) -+ return yaml_data_list -+ except ValidationError as error: -+ current_app.logger.error(error.messages) -+ return None -+ -+ def _save_in_database(self, yaml_data_list): -+ """ -+ Save the data to the database -+ -+ Args: -+ tbname: Table Name -+ name_separate_list: Split name list -+ _update_pack_data: Split new list of combined data -+ -+ Returns: -+ SUCCESS or UPDATA_DATA_FAILED -+ -+ Attributes -+ DisconnectionError: Connect to database error -+ SQLAlchemyError: SQLAlchemy Error -+ Error: Error -+ -+ """ -+ try: -+ with DBHelper(db_name="lifecycle") as database_name: -+ if 'packages_maintainer' not in database_name.engine.table_names(): -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.TABLE_NAME_NOT_EXIST)) -+ database_name.session.begin(subtransactions=True) -+ for yaml_data in yaml_data_list: -+ name = yaml_data.get("name") -+ maintainer = yaml_data.get("maintainer") -+ maintainlevel = yaml_data.get("maintainlevel") -+ packages_maintainer_obj = database_name.session.query( -+ PackagesMaintainer).filter_by(name=name).first() -+ if packages_maintainer_obj: -+ if maintainer: -+ packages_maintainer_obj.maintainer = maintainer -+ if maintainlevel: -+ packages_maintainer_obj.maintainlevel = maintainlevel -+ else: -+ database_name.add(PackagesMaintainer( -+ name=name, maintainer=maintainer, maintainlevel=maintainlevel -+ )) -+ database_name.session.commit() -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.SUCCESS)) -+ except (DisconnectionError, SQLAlchemyError, Error, AttributeError) as error: -+ current_app.logger.error(error) -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.UPDATA_DATA_FAILED)) -+ -+ def _overall_process( -+ self, -+ filepath): -+ """ -+ Call each method to complete the entire function -+ -+ Args: -+ filepath: file path -+ tbname: table name -+ -+ Returns: -+ SUCCESS or UPDATA_DATA_FAILED -+ -+ Attributes -+ DisconnectionError: Connect to database error -+ SQLAlchemyError: SQLAlchemy Error -+ Error: Error -+ """ -+ try: -+ if filepath is None or not os.path.exists(filepath): -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.SPECIFIED_FILE_NOT_EXIST)) -+ yaml_file_list = self._get_all_yaml_name(filepath) -+ if not yaml_file_list: -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.EMPTY_FOLDER)) -+ yaml_data_list_result = self._read_yaml_file(filepath) -+ yaml_data_list = self._verification_yaml_data_list( -+ yaml_data_list_result) -+ if yaml_data_list is None: -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.YAML_FILE_ERROR)) -+ result = self._save_in_database( -+ yaml_data_list) -+ return result -+ except (DisconnectionError, SQLAlchemyError, Error) as error: -+ current_app.logger.error(error) -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.UPDATA_DATA_FAILED)) -+ -+ def _update_single_package_info( -+ self, srcname, maintainer, maintainlevel): -+ """ -+ Update the maintainer field and maintainlevel -+ field of a single package -+ -+ Args: -+ srcname: The name of the source package -+ maintainer: Package maintainer -+ maintainlevel: Package maintenance level -+ -+ Returns: -+ success or failed -+ -+ Attributes -+ SQLAlchemyError: sqlalchemy error -+ DisconnectionError: Cannot connect to database error -+ Error: Error -+ """ -+ if not srcname: -+ return jsonify( -+ ResponseCode.response_json(ResponseCode.PACK_NAME_NOT_FOUND) -+ ) -+ if not maintainer and not maintainlevel: -+ return jsonify( -+ ResponseCode.response_json(ResponseCode.PARAM_ERROR) -+ ) -+ try: -+ with DBHelper(db_name='lifecycle') as database_name: -+ if 'packages_maintainer' not in database_name.engine.table_names(): -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.TABLE_NAME_NOT_EXIST)) -+ update_obj = database_name.session.query( -+ PackagesMaintainer).filter_by(name=srcname).first() -+ if update_obj: -+ if maintainer: -+ update_obj.maintainer = maintainer -+ if maintainlevel: -+ update_obj.maintainlevel = maintainlevel -+ else: -+ database_name.add(PackagesMaintainer( -+ name=srcname, maintainer=maintainer, maintainlevel=maintainlevel -+ )) -+ database_name.session.commit() -+ return jsonify( -+ ResponseCode.response_json( -+ ResponseCode.SUCCESS)) -+ except (SQLAlchemyError, DisconnectionError, Error) as sql_error: -+ current_app.logger.error(sql_error) -+ database_name.session.rollback() -+ return jsonify(ResponseCode.response_json( -+ ResponseCode.UPDATA_DATA_FAILED -+ )) -+ -+ def put(self): -+ """ -+ Life cycle update information of a single package or -+ All packages -+ -+ Returns: -+ for example:: -+ { -+ "code": "", -+ "data": "", -+ "msg": "" -+ } -+ """ -+ schema = UpdatePackagesSchema() -+ data = request.get_json() -+ if schema.validate(data): -+ return jsonify( -+ ResponseCode.response_json(ResponseCode.PARAM_ERROR) -+ ) -+ srcname = data.get('pkg_name', None) -+ maintainer = data.get('maintainer', None) -+ maintainlevel = data.get('maintainlevel', None) -+ batch = data.get('batch') -+ filepath = data.get('filepath', None) -+ -+ if batch: -+ result = self._overall_process(filepath) -+ else: -+ result = self._update_single_package_info( -+ srcname, maintainer, maintainlevel) -+ return result -diff -Naru a/packageship/application/apps/package/function/be_depend.py b/packageship/application/apps/package/function/be_depend.py ---- a/packageship/application/apps/package/function/be_depend.py 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/application/apps/package/function/be_depend.py 2020-09-22 23:48:32.402476132 +0800 -@@ -5,11 +5,12 @@ - This includes both install and build dependencies - Class: BeDepend - """ -+import copy -+from collections import namedtuple, defaultdict - from flask import current_app - from sqlalchemy import text - from sqlalchemy.exc import SQLAlchemyError - from sqlalchemy.sql import literal_column --from packageship.application.apps.package.function.constants import ResponseCode - from packageship.application.models.package import SrcPack - from packageship.libs.dbutils import DBHelper - -@@ -36,6 +37,8 @@ - self.source_name_set = set() - self.bin_name_set = set() - self.result_dict = dict() -+ self.comm_install_builds = defaultdict(set) -+ self.provides_name = set() - - def main(self): - """ -@@ -69,14 +72,16 @@ - [["root", None]] - ] - self.source_name_set.add(self.source_name) -- self.package_bedepend( -+ self._provides_bedepend( - [self.source_name], data_base, package_type='src') - -+ for _, value in self.result_dict.items(): -+ value[-1] = list(value[-1]) - return self.result_dict - -- def package_bedepend(self, pkg_name_list, data_base, package_type): -+ def _get_provides(self, pkg_name_list, data_base, package_type): - """ -- Description: Query the dependent function -+ Description: Query the components provided by the required package - Args: - pkg_name_list:source or binary packages name - data_base: database -@@ -84,35 +89,31 @@ - Returns: - Raises: - SQLAlchemyError: Database connection exception -- """ -- -+ """ -+ res = namedtuple( -+ 'restuple', [ -+ 'search_bin_name', 'search_bin_version', 'source_name']) - sql_com = """ -- SELECT DISTINCT b1.name AS search_bin_name, -+ SELECT DISTINCT b1.name AS search_bin_name, - b1.version AS search_bin_version, - b1.src_name AS source_name, -- b2.name AS bin_name, -- s1.name AS bebuild_src_name, -- b2.src_name AS install_depend_src_name -+ bin_provides.name As pro_name - FROM ( SELECT pkgKey,src_name,name,version FROM bin_pack WHERE {} ) b1 -- LEFT JOIN bin_provides ON bin_provides.pkgKey = b1.pkgKey -- LEFT JOIN bin_requires br ON br.name = bin_provides.name -- LEFT JOIN src_requires sr ON sr.name = bin_provides.name -- LEFT JOIN src_pack s1 ON s1.pkgKey = sr.pkgKey -- LEFT JOIN bin_pack b2 ON b2.pkgKey = br.pkgKey -- """ -+ LEFT JOIN bin_provides ON bin_provides.pkgKey = b1.pkgKey;""" - -+ # package_type - if package_type == 'src': - literal_name = 'src_name' -- - elif package_type == 'bin': - literal_name = 'name' - -- else: -- return -- -+ # Query database -+ # The lower version of SQLite can look up up to 999 parameters -+ # simultaneously, so use 900 sharding queries - try: - result = [] -- for input_name in (pkg_name_list[i:i+900] for i in range(0, len(pkg_name_list), 900)): -+ for input_name in (pkg_name_list[i:i + 900] -+ for i in range(0, len(pkg_name_list), 900)): - name_in = literal_column(literal_name).in_(input_name) - sql_str = text(sql_com.format(name_in)) - result.extend(data_base.session.execute( -@@ -124,74 +125,176 @@ - ).fetchall()) - except SQLAlchemyError as sql_err: - current_app.logger.error(sql_err) -- return ResponseCode.response_json(ResponseCode.CONNECT_DB_ERROR) -+ return - - if not result: - return - -- # Source and binary packages that were found to be dependent -- source_name_list = [] -- bin_name_list = [] -+ # Process the result of the component -+ pro_name_dict = dict() -+ -+ _components = set() - for obj in result: -- if obj.source_name is None: -- source_name = 'NOT FOUND' -- else: -- 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, -+ if not obj.pro_name: -+ continue -+ # De-weight components -+ if obj.pro_name not in self.comm_install_builds: -+ pro_name_dict[obj.pro_name] = res( -+ obj.search_bin_name, obj.search_bin_version, obj.source_name) -+ -+ if obj.search_bin_name not in self.result_dict: -+ self.result_dict[obj.search_bin_name] = [ -+ obj.source_name, - obj.search_bin_version, -- parent_node, -- be_type) -+ self.db_name, -+ self.comm_install_builds[obj.pro_name] -+ if self.comm_install_builds[obj.pro_name] else {(None, None)} -+ ] -+ tmp_ = copy.deepcopy(self.comm_install_builds[obj.pro_name]) - -- if obj.bebuild_src_name not in self.source_name_set: -- self.source_name_set.add(obj.bebuild_src_name) -- source_name_list.append(obj.bebuild_src_name) -- -- 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) -+ tmp_.discard((obj.search_bin_name, 'install')) -+ tmp_.discard((obj.search_bin_name, 'build')) - -- if obj.bin_name not in self.bin_name_set: -- self.bin_name_set.add(obj.bin_name) -- bin_name_list.append(obj.bin_name) -- -- # With_sub_pack=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_name_list.append( -- obj.install_depend_src_name) -- -- if obj.bebuild_src_name is None and obj.bin_name is None: -- parent_node = None -- be_type = None -- self.make_dicts( -- obj.search_bin_name, -- source_name, -- obj.search_bin_version, -- parent_node, -- be_type) -+ if (None, None) in self.result_dict[obj.search_bin_name][-1] \ -+ and self.comm_install_builds[obj.pro_name]: -+ self.result_dict[obj.search_bin_name][-1] = tmp_ -+ else: -+ self.result_dict[obj.search_bin_name][-1].update(tmp_) -+ return pro_name_dict -+ -+ def _provides_bedepend(self, pkg_name_list, data_base, package_type): -+ """ -+ Description: Query the dependent function -+ Args: -+ pkg_name_list:source or binary packages name -+ data_base: database -+ package_type: package type -+ Returns: -+ Raises: -+ SQLAlchemyError: Database connection exception -+ """ -+ # Query component -+ pro_names = self._get_provides(pkg_name_list, data_base, package_type) - -- if len(source_name_list) != 0: -- self.package_bedepend( -+ if not pro_names: -+ return -+ -+ sql_2_bin = """ -+ SELECT DISTINCT -+ b2.name AS bin_name, -+ b2.src_name AS install_depend_src_name, -+ br.name AS pro_name -+ FROM -+ ( SELECT name, pkgKey FROM bin_requires WHERE {}) br -+ LEFT JOIN bin_pack b2 ON b2.pkgKey = br.pkgKey; -+ """ -+ -+ sql_2_src = """ -+ SELECT DISTINCT -+ s1.name AS bebuild_src_name, -+ sr.name AS pro_name -+ FROM -+ ( SELECT name, pkgKey FROM src_requires WHERE {} ) sr -+ LEFT JOIN src_pack s1 ON s1.pkgKey = sr.pkgKey; -+ """ -+ -+ provides_name_list = [pro for pro, _ in pro_names.items()] -+ -+ result_2_bin = [] -+ result_2_src = [] -+ # Query database -+ try: -+ for input_name in ( -+ provides_name_list[i:i + 900] for i in range(0, len(provides_name_list), 900)): -+ name_in = literal_column('name').in_(input_name) -+ sql_str_2_bin = text(sql_2_bin.format(name_in)) -+ result_2_bin.extend(data_base.session.execute( -+ sql_str_2_bin, -+ { -+ 'name_{}'.format(i): v -+ for i, v in enumerate(input_name, 1) -+ } -+ ).fetchall()) -+ sql_str_2src = text(sql_2_src.format(name_in)) -+ result_2_src.extend(data_base.session.execute( -+ sql_str_2src, -+ { -+ 'name_{}'.format(i): v -+ for i, v in enumerate(input_name, 1) -+ } -+ ).fetchall()) -+ except SQLAlchemyError as sql_err: -+ current_app.logger.error(sql_err) -+ return -+ -+ source_name_list = [] -+ bin_name_list = [] -+ -+ # Process the data that the installation depends on -+ for bin_info in result_2_bin: -+ temp_bin_pkg = bin_info.bin_name -+ temp_sub_src_pkg = bin_info.install_depend_src_name -+ -+ #withsubpick ==1 -+ if self.with_sub_pack == '1' and temp_sub_src_pkg not in self.source_name_set: -+ self.source_name_set.add(temp_sub_src_pkg) -+ source_name_list.append(temp_sub_src_pkg) -+ -+ if temp_bin_pkg not in self.bin_name_set: -+ self.bin_name_set.add(temp_bin_pkg) -+ bin_name_list.append(temp_bin_pkg) -+ -+ if bin_info.pro_name not in self.comm_install_builds: -+ self.comm_install_builds[bin_info.pro_name] = { -+ (bin_info.bin_name, 'install') -+ } -+ -+ elif (bin_info.bin_name, 'install') not in \ -+ self.comm_install_builds[bin_info.pro_name]: -+ -+ self.comm_install_builds[bin_info.pro_name].add( -+ (bin_info.bin_name, 'install') -+ ) -+ -+ self.make_dicts( -+ pro_names.get(bin_info.pro_name).search_bin_name, -+ pro_names.get(bin_info.pro_name).source_name, -+ pro_names.get(bin_info.pro_name).search_bin_version, -+ bin_info.bin_name, -+ 'install' -+ ) -+ # Process data that is compile-dependent -+ for src_info in result_2_src: -+ if src_info.bebuild_src_name not in self.source_name_set: -+ self.source_name_set.add(src_info.bebuild_src_name) -+ source_name_list.append(src_info.bebuild_src_name) -+ -+ if src_info.pro_name not in self.comm_install_builds: -+ self.comm_install_builds[src_info.pro_name] = { -+ (src_info.bebuild_src_name, 'build') -+ } -+ elif (src_info.bebuild_src_name, 'build') not in \ -+ self.comm_install_builds[src_info.pro_name]: -+ -+ self.comm_install_builds[src_info.pro_name].add( -+ (src_info.bebuild_src_name, 'build') -+ ) -+ -+ self.make_dicts( -+ pro_names.get(src_info.pro_name).search_bin_name, -+ pro_names.get(src_info.pro_name).source_name, -+ pro_names.get(src_info.pro_name).search_bin_version, -+ src_info.bebuild_src_name, -+ 'build' -+ ) -+ # Recursively query all source packages that need to be looked up -+ if source_name_list: -+ self._provides_bedepend( - source_name_list, data_base, package_type="src") -- if len(bin_name_list) != 0: -- self.package_bedepend(bin_name_list, data_base, package_type="bin") -+ # Recursively query all binary packages that need to be looked up -+ if bin_name_list: -+ self._provides_bedepend( -+ bin_name_list, data_base, package_type="bin") - - def make_dicts(self, key, source_name, version, parent_node, be_type): - """ -@@ -210,29 +313,27 @@ - source_name, - version, - self.db_name, -- [ -- [parent_node, -+ { -+ (parent_node, - be_type -- ] -- ] -+ ) -+ } -+ - ] - else: - if be_type and parent_node: -- if [None, None] in self.result_dict[key][-1]: -- self.result_dict.pop(key) -- self.result_dict[key] = [ -- source_name, -- version, -- self.db_name, -- [ -- [parent_node, -- be_type -- ] -- ] -- ] -+ if (None, None) in self.result_dict[key][-1]: -+ self.result_dict[key][-1] = { -+ ( -+ parent_node, -+ be_type -+ ) -+ } - -- elif [parent_node, be_type] not in self.result_dict[key][-1]: -- self.result_dict[key][-1].append([ -- parent_node, -- be_type -- ]) -+ elif (parent_node, be_type) not in self.result_dict[key][-1]: -+ self.result_dict[key][-1].add( -+ ( -+ parent_node, -+ be_type -+ ) -+ ) -diff -Naru a/packageship/libs/dbutils/sqlalchemy_helper.py b/packageship/libs/dbutils/sqlalchemy_helper.py ---- a/packageship/libs/dbutils/sqlalchemy_helper.py 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/libs/dbutils/sqlalchemy_helper.py 2020-09-22 23:52:23.031681622 +0800 -@@ -9,6 +9,7 @@ - from sqlalchemy.orm import sessionmaker - from sqlalchemy.exc import SQLAlchemyError - from sqlalchemy.exc import DisconnectionError -+from sqlalchemy.exc import OperationalError - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.engine.url import URL - from packageship.libs.exception.ext import Error -@@ -252,6 +253,8 @@ - - except SQLAlchemyError as sql_error: - self.session.rollback() -+ if isinstance(sql_error, OperationalError): -+ raise OperationalError - raise Error(sql_error) - else: - self.session.commit() -diff -Naru a/packageship/pkgshipd b/packageship/pkgshipd ---- a/packageship/pkgshipd 2020-09-22 23:34:04.037937224 +0800 -+++ b/packageship/pkgshipd 2020-09-22 23:51:57.323547247 +0800 -@@ -1,6 +1,18 @@ - #!/bin/bash - SYS_PATH=/etc/pkgship - OUT_PATH=/var/run/pkgship_uwsgi -+ -+MEM_THRESHOLD='700' -+MEM_FREE=`free -m | grep "Mem" | awk '{print $7}'` -+ -+if [ $1 = "start" ] -+then -+ if [ $MEM_FREE -lt $MEM_THRESHOLD ]; then -+ echo "[ERROR] pkgship tool does not support memory less than ${MEM_THRESHOLD} MB." -+ exit 0 -+ fi -+fi -+ - if [ ! -d "$OUT_PATH" ]; then - mkdir $OUT_PATH - fi -diff -Naru a/test/common_files/package.ini b/test/common_files/package.ini ---- a/test/common_files/package.ini 2020-09-22 23:34:04.041937245 +0800 -+++ b/test/common_files/package.ini 2020-09-22 23:50:56.559229634 +0800 -@@ -1,30 +1,30 @@ --[SYSTEM] --init_conf_path = C:\Users\TAO\Desktop\pkgship-1.1.0\test\common_files\conf.yaml --write_port = 8080 --query_port = 8090 --write_ip_addr = 127.0.0.1 --query_ip_addr = 127.0.0.1 --remote_host = https://api.openeuler.org/pkgmanage -- --[LOG] --log_level = INFO --log_name = log_info.log --backup_count = 10 --max_bytes = 314572800 -- --[UWSGI] --daemonize = /var/log/uwsgi.log --buffer-size = 65536 --http-timeout = 600 --harakiri = 600 -- --[TIMEDTASK] --open = True --hour = 3 --minute = 0 -- --[LIFECYCLE] --warehouse_remote = https://gitee.com/openeuler/openEuler-Advisor/raw/master/upstream-info/ --pool_workers = 10 --warehouse = src-openeuler -- -+[SYSTEM] -+init_conf_path = -+write_port = 8080 -+query_port = 8090 -+write_ip_addr = 127.0.0.1 -+query_ip_addr = 127.0.0.1 -+remote_host = https://api.openeuler.org/pkgmanage -+ -+[LOG] -+log_level = INFO -+log_name = log_info.log -+backup_count = 10 -+max_bytes = 314572800 -+ -+[UWSGI] -+daemonize = /var/log/uwsgi.log -+buffer-size = 65536 -+http-timeout = 600 -+harakiri = 600 -+ -+[TIMEDTASK] -+open = True -+hour = 3 -+minute = 0 -+ -+[LIFECYCLE] -+warehouse_remote = https://gitee.com/openeuler/openEuler-Advisor/raw/master/upstream-info/ -+pool_workers = 10 -+warehouse = src-openeuler -+ diff --git a/0007-correct-the-parameter-transfer-method-and-change-the-status-recording-method.patch b/0007-correct-the-parameter-transfer-method-and-change-the-status-recording-method.patch deleted file mode 100644 index f3f6480..0000000 --- a/0007-correct-the-parameter-transfer-method-and-change-the-status-recording-method.patch +++ /dev/null @@ -1,211 +0,0 @@ -diff -Naru pkgship-1.1.0/packageship/application/apps/package/function/build_depend.py pkg/packageship/application/apps/package/function/build_depend.py ---- pkgship-1.1.0/packageship/application/apps/package/function/build_depend.py 2020-10-13 13:57:13.529049796 +0800 -+++ pkg/packageship/application/apps/package/function/build_depend.py 2020-10-13 13:58:37.670278333 +0800 -@@ -89,9 +89,9 @@ - build_list, - not_fd_com_build, - pk_v -- ) = self.search_db.get_build_depend(pkg_list, self.__already_pk_val) -+ ) = self.search_db.get_build_depend(pkg_list, pk_value=self.__already_pk_val) - -- self.__already_pk_val += pk_v -+ self.__already_pk_val = pk_v - self.not_found_components.update(not_fd_com_build) - if not build_list: - return res_status if res_status == ResponseCode.DIS_CONNECTION_DB else \ -@@ -102,8 +102,8 @@ - - code, res_dict, not_fd_com_install = \ - InstallDepend(self.db_list).query_install_depend(search_list, -- self.history_dicts, -- self.__already_pk_val) -+ history_pk_val=self.__already_pk_val, -+ history_dicts=self.history_dicts) - self.not_found_components.update(not_fd_com_install) - if not res_dict: - return code -@@ -206,8 +206,8 @@ - not_fd_com, - pk_v - ) = self.search_db.get_build_depend(pkg_name_li, -- self.__already_pk_val) -- self.__already_pk_val += pk_v -+ pk_value=self.__already_pk_val) -+ self.__already_pk_val = pk_v - self.not_found_components.update(not_fd_com) - if not bin_info_lis: - return -diff -Naru pkgship-1.1.0/packageship/application/apps/package/function/install_depend.py pkg/packageship/application/apps/package/function/install_depend.py ---- pkgship-1.1.0/packageship/application/apps/package/function/install_depend.py 2020-10-13 13:57:13.529049796 +0800 -+++ pkg/packageship/application/apps/package/function/install_depend.py 2020-10-13 13:58:37.680278477 +0800 -@@ -68,7 +68,7 @@ - self.__search_list.append(binary) - else: - LOGGER.logger.warning("There is a NONE in input value: %s", str(binary_list)) -- self.__already_pk_value += history_pk_val if history_pk_val else [] -+ self.__already_pk_value = history_pk_val if history_pk_val else [] - while self.__search_list: - self.__query_single_install_dep(history_dicts) - return ResponseCode.SUCCESS, self.binary_dict.dictionary, self.not_found_components -@@ -82,14 +82,11 @@ - response_code: response code - Raises: - """ -- result_list, not_found_components, pk_val = map( -- set, -- self.__search_db.get_install_depend(self.__search_list, -- self.__already_pk_value) -- ) -- -+ res_list, not_found_components, pk_val = self.__search_db.get_install_depend(self.__search_list, -+ pk_value=self.__already_pk_value) -+ result_list = set(res_list) - self.not_found_components.update(not_found_components) -- self.__already_pk_value += pk_val -+ self.__already_pk_value = pk_val - for search in self.__search_list: - if search not in self.binary_dict.dictionary: - self.binary_dict.init_key(key=search, parent_node=[]) -diff -Naru pkgship-1.1.0/packageship/application/apps/package/function/searchdb.py pkg/packageship/application/apps/package/function/searchdb.py ---- pkgship-1.1.0/packageship/application/apps/package/function/searchdb.py 2020-10-13 13:57:13.529049796 +0800 -+++ pkg/packageship/application/apps/package/function/searchdb.py 2020-10-13 13:58:37.680278477 +0800 -@@ -94,7 +94,7 @@ - - for db_name, data_base in self.db_object_dict.items(): - try: -- req_set = self._get_requires(search_set, data_base, _tp='install') -+ req_set = self._get_requires(search_set, data_base, search_type='install') - - if not req_set: - continue -@@ -104,7 +104,7 @@ - pk_v, - not_fd_com) = self._get_provides_req_info(req_set, - data_base, -- pk_val) -+ pk_value=pk_val) - pk_val += pk_v - res_list, get_list = self._comb_install_list(depend_set, - req_pk_dict, -@@ -121,7 +121,7 @@ - if not search_set: - result_list.extend( - self._get_install_pro_in_other_database(provides_not_found, -- db_name) -+ database_name=db_name) - ) - return result_list, set(provides_not_found.keys()), pk_val - -@@ -215,13 +215,13 @@ - - return ret_list, get_list - -- def _get_install_pro_in_other_database(self, not_found_binary, _db_name=None): -+ def _get_install_pro_in_other_database(self, not_found_binary, database_name=None): - """ - 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_name:current database name -+ database_name:current database name - Returns: - result_list :[return_tuple1,return_tuple2] package information - Raises: -@@ -242,7 +242,7 @@ - search_set = {k for k, _ in not_found_binary.items()} - - for db_name, data_base in self.db_object_dict.items(): -- if db_name == _db_name: -+ if db_name == database_name: - continue - - parm_tuple = namedtuple("in_tuple", 'req_name') -@@ -362,7 +362,7 @@ - for db_name, data_base in self.db_object_dict.items(): - - try: -- req_set = self._get_requires(s_name_set, data_base, _tp='build') -+ req_set = self._get_requires(s_name_set, data_base, search_type='build') - - if not req_set: - continue -@@ -384,7 +384,7 @@ - s_name_set.symmetric_difference_update(set(get_list)) - if not s_name_set: - build_list.extend( -- self._get_binary_in_other_database(provides_not_found, _db_name=db_name) -+ self._get_binary_in_other_database(provides_not_found, database_name=db_name) - ) - return ResponseCode.SUCCESS, build_list, set(provides_not_found.keys()), pk_val - -@@ -483,13 +483,13 @@ - - return ret_list, get_list - -- def _get_binary_in_other_database(self, not_found_binary, _db_name=None): -+ def _get_binary_in_other_database(self, not_found_binary, database_name=None): - """ - 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_name:current database name -+ database_name:current database name - Returns: - result_list :[return_tuple1,return_tuple2] package information - Raises: -@@ -513,7 +513,7 @@ - - for db_name, data_base in self.db_object_dict.items(): - -- if db_name == _db_name: -+ if db_name == database_name: - continue - - in_tuple = namedtuple("in_tuple", 'req_name') -@@ -600,20 +600,20 @@ - - # Common methods for install and build - @staticmethod -- def _get_requires(search_set, data_base, _tp=None): -+ def _get_requires(search_set, data_base, search_type=None): - """ - Description: Query the dependent components of the current package - Args: - search_set: The package name to be queried - data_base:current database object -- _tp:type options build or install -+ search_type: type options build or install - Returns: - req_set:List Package information and corresponding component information - Raises: - AttributeError: The object does not have this property - SQLAlchemyError: sqlalchemy error - """ -- if _tp == 'build': -+ if search_type == 'build': - sql_com = text(""" - SELECT DISTINCT - src_requires.NAME AS req_name, -@@ -623,7 +623,7 @@ - ( SELECT pkgKey, NAME, version, src_name FROM src_pack WHERE {} ) src - LEFT JOIN src_requires ON src.pkgKey = src_requires.pkgKey; - """.format(literal_column('name').in_(search_set))) -- elif _tp == 'install': -+ elif search_type == 'install': - sql_com = text(""" - SELECT DISTINCT - bin_requires.NAME AS req_name, -diff -Naru pkgship-1.1.0/packageship/application/apps/package/function/self_depend.py pkg/packageship/application/apps/package/function/self_depend.py ---- pkgship-1.1.0/packageship/application/apps/package/function/self_depend.py 2020-10-13 13:57:13.529049796 +0800 -+++ pkg/packageship/application/apps/package/function/self_depend.py 2020-10-13 13:58:37.690278620 +0800 -@@ -143,7 +143,7 @@ - self.result_tmp.clear() - _, self.result_tmp, not_fd_com = \ - install_depend(self.db_list).query_install_depend(self.search_install_list, -- self.binary_dict.dictionary) -+ history_dicts=self.binary_dict.dictionary) - self.not_found_components.update(not_fd_com) - self.search_install_list.clear() - for key, values in self.result_tmp.items(): diff --git a/0008-fix-selfbuild-error-message.patch b/0008-fix-selfbuild-error-message.patch deleted file mode 100644 index 30ec7a4..0000000 --- a/0008-fix-selfbuild-error-message.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff -Naru pkgship-1.1.0/packageship/application/apps/package/function/searchdb.py pkgship/packageship/application/apps/package/function/searchdb.py ---- pkgship-1.1.0/packageship/application/apps/package/function/searchdb.py 2020-09-25 17:28:16.230216100 +0800 -+++ pkgship/packageship/application/apps/package/function/searchdb.py 2020-09-25 17:30:48.456873100 +0800 -@@ -909,6 +909,8 @@ - current_app.logger.error(attr_error) - except SQLAlchemyError as sql_error: - current_app.logger.error(sql_error) -+ if not result_list: -+ return ResponseCode.PACK_NAME_NOT_FOUND, result_list - return_tuple = namedtuple( - 'return_tuple', 'subpack_name sub_pack_version search_version search_name') - for search_name in search_set: diff --git a/0009-optimize-log-records-when-obtaining-issue-content.patch b/0009-optimize-log-records-when-obtaining-issue-content.patch deleted file mode 100644 index 2b6c0cc..0000000 --- a/0009-optimize-log-records-when-obtaining-issue-content.patch +++ /dev/null @@ -1,134 +0,0 @@ -diff --git a/packageship/application/apps/lifecycle/function/gitee.py b/packageship/application/apps/lifecycle/function/gitee.py -index 4ac077f..8ca4ccf 100644 ---- a/packageship/application/apps/lifecycle/function/gitee.py -+++ b/packageship/application/apps/lifecycle/function/gitee.py -@@ -8,6 +8,7 @@ from json import JSONDecodeError - from retrying import retry - import requests - from requests.exceptions import HTTPError -+from requests.exceptions import RequestException - from sqlalchemy.exc import SQLAlchemyError - from packageship.libs.dbutils import DBHelper - from packageship.libs.configutils.readconfig import ReadConfig -@@ -42,6 +43,8 @@ class Gitee(): - "patch_files_path") - self.table_name = table_name - self.producer_consumer = ProducerConsumer() -+ self._issue_url = None -+ self.total_page = 0 - - def query_issues_info(self, issue_id=""): - """ -@@ -53,55 +56,58 @@ class Gitee(): - Raises: - - """ -- issue_url = self.api_url + \ -- "/{}/{}/issues/{}".format(self.owner, self.repo, issue_id) -+ self._issue_url = self.api_url + \ -+ "/{}/{}/issues/{}".format(self.owner, self.repo, issue_id) - try: -- response = requests.get( -- issue_url, params={"state": "all", "per_page": 100}) -- except Error as error: -+ response = self._request_issue(0) -+ except (HTTPError, RequestException) as error: - LOGGER.logger.error(error) - return None -- if response.status_code != 200: -- return None -- total_page = 1 if issue_id else int(response.headers['total_page']) -+ -+ self.total_page = 1 if issue_id else int( -+ response.headers['total_page']) - total_count = int(response.headers['total_count']) -+ - if total_count > 0: -- issue_list = self._query_per_page_issue_info(total_page, issue_url) -+ issue_list = self._query_per_page_issue_info() - if not issue_list: - LOGGER.logger.error( - "An error occurred while querying {}".format(self.repo)) - return None - self._save_issues(issue_list) - -- def _query_per_page_issue_info(self, total_page, issue_url): -+ @retry(stop_max_attempt_number=3, stop_max_delay=1000) -+ def _request_issue(self, page): -+ try: -+ response = requests.get(self._issue_url, -+ params={"state": "all", "per_page": 100, "page": page}) -+ except RequestException as error: -+ raise RequestException(error) -+ if response.status_code != 200: -+ _msg = "There is an exception with the remote service [%s]," \ -+ "Please try again later.The HTTP error code is:%s" % (self._issue_url, str( -+ response.status_code)) -+ raise HTTPError(_msg) -+ return response -+ -+ def _query_per_page_issue_info(self): - """ - Description: View the issue details - Args: - total_page: total page -- issue_url: issue url - - Returns: - - """ - issue_content_list = [] -- for i in range(1, total_page + 1): -- -- @retry(stop_max_attempt_number=3, stop_max_delay=1000) -- def request_issue(page, issue_url): -- try: -- response = requests.get(issue_url, -- params={"state": "all", "per_page": 100, "page": page}) -- except HTTPError: -- raise HTTPError('Network request error') -- return response -- -+ for i in range(1, self.total_page + 1): - try: -- response = request_issue(i, issue_url) -- if response.status_code != 200: -- LOGGER.logger.warning(response.content.decode("utf-8")) -- continue -+ response = self._request_issue(i) - issue_content_list.extend( - self.parse_issues_content(response.json())) -+ except (HTTPError, RequestException) as error: -+ LOGGER.logger.error(error) -+ continue - except (JSONDecodeError, Error) as error: - LOGGER.logger.error(error) - return issue_content_list -@@ -114,12 +120,9 @@ class Gitee(): - try: - def _save(issue_module): - with DBHelper(db_name='lifecycle') as database: -- - exist_issues = database.session.query(PackagesIssue).filter( - PackagesIssue.issue_id == issue_module['issue_id']).first() - if exist_issues: -- -- # Save the issue - for key, val in issue_module.items(): - setattr(exist_issues, key, val) - else: -@@ -130,11 +133,11 @@ class Gitee(): - with DBHelper(db_name='lifecycle') as database: - database.add(package_module) - -+ # Save the issue - for issue_item in issue_list: -- self.producer_consumer.put( -- (copy.deepcopy(issue_item), _save)) -+ self.producer_consumer.put((copy.deepcopy(issue_item), _save)) - -- # The number of various issues in the update package -+ # The number of various issues in the update package - self.pkg_info.defect = self.defect - self.pkg_info.feature = self.feature - self.pkg_info.cve = self.cve diff --git a/pkgship-1.1.0.tar.gz b/pkgship-1.1.0.tar.gz deleted file mode 100644 index 9e45007af4b886171379b56d7a74feb9acce0481..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7555620 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