From 7b481121d4359acb39202c5d1ceb3dc40116aa66 Mon Sep 17 00:00:00 2001 From: custa <> Date: Tue, 21 Jul 2020 21:25:58 +0800 Subject: [PATCH 1/6] patch tracking codebase --- patch-tracking/.gitignore | 70 +++ patch-tracking/.pylintrc | 595 ++++++++++++++++++ patch-tracking/.style.yapf | 4 + patch-tracking/Pipfile | 20 + patch-tracking/Pipfile.lock | 421 +++++++++++++ patch-tracking/README.md | 251 ++++++++ patch-tracking/images/Maintainer.jpg | Bin 0 -> 46243 bytes patch-tracking/images/PatchTracking.jpg | Bin 0 -> 40635 bytes patch-tracking/patch-tracking.spec | 58 ++ patch-tracking/patch_tracking/__init__.py | 0 patch-tracking/patch_tracking/api/__init__.py | 0 patch-tracking/patch_tracking/api/business.py | 78 +++ patch-tracking/patch_tracking/api/constant.py | 50 ++ patch-tracking/patch_tracking/api/issue.py | 34 + patch-tracking/patch_tracking/api/tracking.py | 83 +++ patch-tracking/patch_tracking/app.py | 59 ++ patch-tracking/patch_tracking/cli/__init__.py | 0 .../patch_tracking/cli/generate_password | 64 ++ .../patch_tracking/cli/patch-tracking-cli | 11 + .../patch_tracking/cli/patch_tracking_cli.py | 283 +++++++++ .../patch_tracking/database/__init__.py | 14 + .../patch_tracking/database/models.py | 67 ++ .../patch_tracking/database/reset_db.py | 17 + patch-tracking/patch_tracking/db.sqlite | Bin 0 -> 20480 bytes patch-tracking/patch_tracking/logging.conf | 29 + patch-tracking/patch_tracking/patch-tracking | 11 + .../patch_tracking/patch-tracking.service | 16 + patch-tracking/patch_tracking/self-signed.crt | 30 + patch-tracking/patch_tracking/self-signed.key | 52 ++ patch-tracking/patch_tracking/settings.conf | 17 + .../patch_tracking/task/__init__.py | 6 + patch-tracking/patch_tracking/task/task.py | 97 +++ .../patch_tracking/task/task_apscheduler.py | 265 ++++++++ .../patch_tracking/tests/issue_test.py | 191 ++++++ .../patch_tracking/tests/logging.conf | 22 + .../patch_tracking/tests/tracking_test.py | 400 ++++++++++++ .../patch_tracking/util/__init__.py | 0 patch-tracking/patch_tracking/util/auth.py | 19 + .../patch_tracking/util/gitee_api.py | 137 ++++ .../patch_tracking/util/github_api.py | 118 ++++ patch-tracking/patch_tracking/util/spec.py | 121 ++++ patch-tracking/setup.py | 27 + 42 files changed, 3737 insertions(+) create mode 100644 patch-tracking/.gitignore create mode 100644 patch-tracking/.pylintrc create mode 100644 patch-tracking/.style.yapf create mode 100644 patch-tracking/Pipfile create mode 100644 patch-tracking/Pipfile.lock create mode 100644 patch-tracking/README.md create mode 100644 patch-tracking/images/Maintainer.jpg create mode 100644 patch-tracking/images/PatchTracking.jpg create mode 100644 patch-tracking/patch-tracking.spec create mode 100644 patch-tracking/patch_tracking/__init__.py create mode 100644 patch-tracking/patch_tracking/api/__init__.py create mode 100644 patch-tracking/patch_tracking/api/business.py create mode 100644 patch-tracking/patch_tracking/api/constant.py create mode 100644 patch-tracking/patch_tracking/api/issue.py create mode 100644 patch-tracking/patch_tracking/api/tracking.py create mode 100644 patch-tracking/patch_tracking/app.py create mode 100644 patch-tracking/patch_tracking/cli/__init__.py create mode 100644 patch-tracking/patch_tracking/cli/generate_password create mode 100644 patch-tracking/patch_tracking/cli/patch-tracking-cli create mode 100755 patch-tracking/patch_tracking/cli/patch_tracking_cli.py create mode 100644 patch-tracking/patch_tracking/database/__init__.py create mode 100644 patch-tracking/patch_tracking/database/models.py create mode 100644 patch-tracking/patch_tracking/database/reset_db.py create mode 100644 patch-tracking/patch_tracking/db.sqlite create mode 100644 patch-tracking/patch_tracking/logging.conf create mode 100755 patch-tracking/patch_tracking/patch-tracking create mode 100644 patch-tracking/patch_tracking/patch-tracking.service create mode 100644 patch-tracking/patch_tracking/self-signed.crt create mode 100644 patch-tracking/patch_tracking/self-signed.key create mode 100644 patch-tracking/patch_tracking/settings.conf create mode 100644 patch-tracking/patch_tracking/task/__init__.py create mode 100644 patch-tracking/patch_tracking/task/task.py create mode 100644 patch-tracking/patch_tracking/task/task_apscheduler.py create mode 100644 patch-tracking/patch_tracking/tests/issue_test.py create mode 100644 patch-tracking/patch_tracking/tests/logging.conf create mode 100644 patch-tracking/patch_tracking/tests/tracking_test.py create mode 100644 patch-tracking/patch_tracking/util/__init__.py create mode 100644 patch-tracking/patch_tracking/util/auth.py create mode 100644 patch-tracking/patch_tracking/util/gitee_api.py create mode 100644 patch-tracking/patch_tracking/util/github_api.py create mode 100644 patch-tracking/patch_tracking/util/spec.py create mode 100644 patch-tracking/setup.py diff --git a/patch-tracking/.gitignore b/patch-tracking/.gitignore new file mode 100644 index 00000000..283bf0a0 --- /dev/null +++ b/patch-tracking/.gitignore @@ -0,0 +1,70 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# pyenv +.python-version + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Editors +.idea/ + + +# log file +*.log diff --git a/patch-tracking/.pylintrc b/patch-tracking/.pylintrc new file mode 100644 index 00000000..856a9762 --- /dev/null +++ b/patch-tracking/.pylintrc @@ -0,0 +1,595 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=issue_test,tracking_test + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/patch-tracking/.style.yapf b/patch-tracking/.style.yapf new file mode 100644 index 00000000..1c04a76b --- /dev/null +++ b/patch-tracking/.style.yapf @@ -0,0 +1,4 @@ +[style] +based_on_style = pep8 +column_limit = 120 +dedent_closing_brackets = True diff --git a/patch-tracking/Pipfile b/patch-tracking/Pipfile new file mode 100644 index 00000000..13c8419e --- /dev/null +++ b/patch-tracking/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +verify_ssl = true + +[dev-packages] +pylint = "*" +yapf = "*" +pyopenssl = "*" + +[packages] +flask = "*" +flask-sqlalchemy = "*" +flask-apscheduler = "*" +requests = "*" +werkzeug = "*" +flask-httpauth = "*" + +[requires] +python_version = "3.7" diff --git a/patch-tracking/Pipfile.lock b/patch-tracking/Pipfile.lock new file mode 100644 index 00000000..9e63ae94 --- /dev/null +++ b/patch-tracking/Pipfile.lock @@ -0,0 +1,421 @@ +{ + "_meta": { + "hash": { + "sha256": "a7833948fd05f098923413c1dadff35d6e08fad526d0ccb93a4b60f73b9f9f24" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.tuna.tsinghua.edu.cn/simple", + "verify_ssl": true + } + ] + }, + "default": { + "apscheduler": { + "hashes": [ + "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244", + "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526" + ], + "version": "==3.6.3" + }, + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "flask": { + "hashes": [ + "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", + "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" + ], + "index": "pypi", + "version": "==1.1.2" + }, + "flask-apscheduler": { + "hashes": [ + "sha256:7911d66e449f412d92a1a6c524217f44f4c40a5c92148c60d5189c6c402f87d0" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "flask-httpauth": { + "hashes": [ + "sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7", + "sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000" + ], + "index": "pypi", + "version": "==4.1.0" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:0b656fbf87c5f24109d859bafa791d29751fabbda2302b606881ae5485b557a5", + "sha256:fcfe6df52cd2ed8a63008ca36b86a51fa7a4b70cef1c39e5625f722fca32308e" + ], + "index": "pypi", + "version": "==2.4.3" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", + "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.11.2" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.1" + }, + "pytz": { + "hashes": [ + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", + "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + ], + "version": "==2020.1" + }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "index": "pypi", + "version": "==2.24.0" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e", + "sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772", + "sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7", + "sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf", + "sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98", + "sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864", + "sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9", + "sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1", + "sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd", + "sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4", + "sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1", + "sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c", + "sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8", + "sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e", + "sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce", + "sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1", + "sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5", + "sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe", + "sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413", + "sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3", + "sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284", + "sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1", + "sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7", + "sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299", + "sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33", + "sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d", + "sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274", + "sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.3.18" + }, + "tzlocal": { + "hashes": [ + "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", + "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4" + ], + "version": "==2.1" + }, + "urllib3": { + "hashes": [ + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.9" + }, + "werkzeug": { + "hashes": [ + "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", + "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" + ], + "index": "pypi", + "version": "==1.0.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" + ], + "markers": "python_version >= '3.5'", + "version": "==2.4.2" + }, + "cffi": { + "hashes": [ + "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", + "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", + "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", + "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", + "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", + "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", + "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", + "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", + "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", + "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", + "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", + "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", + "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", + "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", + "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", + "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", + "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", + "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", + "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", + "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", + "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", + "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", + "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", + "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", + "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", + "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", + "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", + "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" + ], + "version": "==1.14.0" + }, + "cryptography": { + "hashes": [ + "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", + "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", + "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", + "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", + "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", + "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", + "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", + "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", + "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", + "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", + "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", + "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", + "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", + "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", + "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", + "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", + "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", + "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", + "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.9.2" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.3.21" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.3" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, + "pylint": { + "hashes": [ + "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", + "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" + ], + "index": "pypi", + "version": "==2.5.3" + }, + "pyopenssl": { + "hashes": [ + "sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504", + "sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507" + ], + "index": "pypi", + "version": "==19.1.0" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "markers": "python_version < '3.8' and implementation_name == 'cpython'", + "version": "==1.4.1" + }, + "wrapt": { + "hashes": [ + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + ], + "version": "==1.12.1" + }, + "yapf": { + "hashes": [ + "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427", + "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9" + ], + "index": "pypi", + "version": "==0.30.0" + } + } +} diff --git a/patch-tracking/README.md b/patch-tracking/README.md new file mode 100644 index 00000000..3b360b4a --- /dev/null +++ b/patch-tracking/README.md @@ -0,0 +1,251 @@ +补丁跟踪 +=== + + +# 一 简介 + +在 openEuler 发行版开发过程,需要及时更新上游社区各个软件包的最新代码,修改功能 bug 及安全问题,确保发布的 openEuler 发行版尽可能避免缺陷和漏洞。 + +本工具对软件包进行补丁管理,主动监控上游社区提交,自动生成补丁,并自动提交 issue 给对应的 maintainer,同时自动验证补丁基础功能,减少验证工作量支持 maintainer 快速决策。 + +# 二 架构 + +### 2.1 CS架构 + +补丁跟踪采用 C/S 架构,其中服务端(patch-tracking) 负责执行补丁跟踪任务,包括:维护跟踪项,识别上游仓库分支代码变更并形成补丁文件,向 Gitee 提交 issue 及 PR,同时 patch-tracking 提供 RESTful 接口,用于对跟踪项进行增删改查操作。客户端,即命令行工具(patch-tracking-cli),通过调用 patch-tracking 的 RESTful 接口,实现对跟踪项的增删改查操作。 + +### 2.2 核心流程 + +* 补丁跟踪服务流程 + +**主要步骤:** +1. 命令行工具写入跟踪项。 +2. 自动从跟踪项配置的上游仓库(例如Github)获取补丁文件。 +3. 创建临时分支,将获取到的补丁文件提交到临时分支。 +4. 自动提交issue到对应项目,并生成关联 issue 的 PR。 + +![PatchTracking](images/PatchTracking.jpg) + +* Maintainer对提交的补丁处理流程 + +**主要步骤:** +1. Maintainer分析临时分支中的补丁文件,判断是否合入。 +2. 执行构建,构建成功后判断是否合入PR。 + +![Maintainer](images/Maintainer.jpg) + +### 2.3 数据结构 + +* Tracking表 + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | +|:----:| ----| ----| ----| ----| ----| +| 1 | id | 自增补丁跟踪项序号 | int | - | NO | +| 2 | version_control | 上游SCM的版本控制系统类型 | String | - | NO | +| 3 | scm_repo | 上游SCM仓库地址 | String | - | NO | +| 4 | scm_branch | 上游SCM跟踪分支 | String | - | NO | +| 5 | scm_commit | 上游代码最新处理过的Commit ID | String | - | YES | +| 6 | repo | 包源码在Gitee的仓库地址 | String | Primary | NO | +| 7 | branch | 包源码在Gitee的仓库分支 | String | Primary | NO | +| 8 | enabled | 是否启动跟踪 | Boolean | -| NO | + +* Issue表 + +| 序号 | 名称 | 说明 | 类型 | 键 | 允许空 | +|:----:| ----| ----| ----| ----| ----| +| 1 | issue | issue编号 | String | Primary | NO | +| 2 | repo | 包源码在Gitee的仓库地址 | String | - | NO | +| 3 | branch | 包源码在Gitee的仓库分支 | String | - | NO | + +# 三 部署 + +>环境已安装 Python >= 3.7 以及 pip3 + +### 3.1 安装依赖 + +```shell script +yum install -y gcc python3-devel openssl-devel +pip3 install flask flask-sqlalchemy flask-apscheduler requests flask_httpauth +pip3 install -I uwsgi +``` + + +### 3.2 安装 + +```shell script +rpm -ivh patch-tracking-xxx.rpm +``` + +### 3.3 配置 + +在配置文件中进行对应参数的配置。 + +配置文件路径 `/etc/patch-tracking/settings.conf`。 + + +- 服务监听地址 + +```python +LISTEN = "127.0.0.1:5001" +``` + +- GitHub Token,用于访问托管在 GitHub 上游开源软件仓的仓库信息 + +生成 GitHub Token 的方法参考 [Creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) + +```python +GITHUB_ACCESS_TOKEN = "" +``` + +- 对于托管在gitee上的需要跟踪的仓库,配置一个有该仓库权限的gitee的token,用于提交patch文件,提交issue,提交PR等操作。 + +```python +GITEE_ACCESS_TOKEN = "" +``` + +- 定时扫描数据库中是否有新增或修改的跟踪项,对扫描到的跟踪项执行获取上游补丁任务,在这里配置扫描的时间间隔,数字单位是秒 + +```python +SCAN_DB_INTERVAL = 3600 +``` + +- 命令行工具运行过程中,POST接口需要进行认证的用户名和密码 + +```python +USER = "admin" + +PASSWORD = "" +``` + +`USER`默认值为`admin`。 + +>`PASSWORD`口令的复杂度要求: +>* 长度大于等于6个字符 +>* 至少有一个数字 +>* 至少有一个大写字母 +>* 至少有一个小写字母 +>* 至少有一个特殊字符 (~!@#%^*_+=-) + +需要将口令的哈希值通过命令工具生成后将其配置到此处,获取方法为执行命令`generate_password `,例如: + + [root]# generate_password Test@123 + pbkdf2:sha256:150000$w38eLeRm$ebb5069ba3b4dda39a698bd1d9d7f5f848af3bd93b11e0cde2b28e9e34bfbbae + +将`pbkdf2:sha256:150000$w38eLeRm$ebb5069ba3b4dda39a698bd1d9d7f5f848af3bd93b11e0cde2b28e9e34bfbbae`配置到`PASSWORD = ""`引号中。 + +### 3.4 启动补丁跟踪服务 + +可以使用以下两种方式启动服务: + +1. 使用 systemd 方式 + +```shell script +systemctl start patch-tracking +``` + +2. 直接执行可执行程序 + +```shell script +/usr/bin/patch-tracking +``` + +# 四 使用 + +### 4.1 添加跟踪项 + +将需要跟踪的软件仓库和分支与其上游开源软件仓库与分支关联起来,有 3 种使用方法。 + +#### 4.1.1 命令行直接添加 + +参数含义: +>--user :POST接口需要进行认证的用户名,同settings.conf中的USER参数 \ +--password :POST接口需要进行认证的口令,为settings.conf中的PASSWORD哈希值对应的实际的口令字符串 \ +--server :启动Patch Tracking服务的URL,例如:127.0.0.1:5001 \ +--version_control :上游仓库版本的控制工具,只支持github \ +--repo 需要进行跟踪的仓库名称,格式:组织/仓库 \ +--branch 需要进行跟踪的仓库的分支名称 \ +--scm_repo 被跟踪的上游仓库的仓库名称,github格式:组织/仓库 \ +--scm_branch 被跟踪的上游仓库的仓库的分支 \ +--enable 是否自动跟踪该仓库 + +例如: +```shell script +patch-tracking-cli --server 127.0.0.1:5001 --user admin --password Test@123 --version_control github --repo testPatchTrack/testPatch1 --branch master --scm_repo BJMX/testPatch01 --scm_branch test --enable true +``` + +#### 4.1.2 指定文件添加 + +参数含义: +>--server :启动Patch Tracking服务的URL,例如:127.0.0.1:5001 \ +--user :POST接口需要进行认证的用户名,同settings.conf中的USER参数 \ +--password :POST接口需要进行认证的口令,为settings.conf中的PASSWORD哈希值对应的实际的口令字符串 \ +--file :yaml文件路径 + +文件内容是仓库、分支、版本管理工具、是否启动监控等信息,将这些写入文件名为xxx.yaml,例如tracking.yaml,文件路径作为`--file`的入参调用命令。 + +例如: +```shell script +patch-tracking-cli --server 127.0.0.1:5001 --user admin --password Test@123 --file tracking.yaml +``` + +yaml内容格式如下,冒号左边的内容不可修改,右边内容根据实际情况填写。 + +```shell script +version_control: github +scm_repo: xxx/xxx +scm_branch: master +repo: xxx/xxx +branch: master +enabled: true +``` + +>version_control :上游仓库版本的控制工具,只支持github \ +scm_repo 被跟踪的上游仓库的仓库名称,github格式:组织/仓库 \ +scm_branch 被跟踪的上游仓库的仓库的分支 \ +repo 需要进行跟踪的仓库名称,格式:组织/仓库 \ +branch 需要进行跟踪的仓库的分支名称 \ +enable 是否自动跟踪该仓库 + +#### 4.1.3 指定目录添加 + +在指定的目录,例如`test_yaml`下放入多个`xxx.yaml`文件,执行命令,记录指定目录下所有yaml文件的跟踪项。yaml文件都放在不会读取子目录内文件。, + +参数含义: +>--user :POST接口需要进行认证的用户名,同settings.conf中的USER参数 \ +--password :POST接口需要进行认证的口令,为settings.conf中的PASSWORD哈希值对应的实际的口令字符串 \ +--server :启动Patch Tracking服务的URL,例如:127.0.0.1:5001 \ +--dir :存放yaml文件目录的路径 + +```shell script +patch-tracking-cli --server 127.0.0.1:5001 --user admin --password Test@123 --dir /home/Work/test_yaml/ +``` + +### 4.2 查询跟踪项 + +```shell script +curl -k https:///tracking +``` +例如: +```shell script +curl -k https://127.0.0.1:5001/tracking +``` + +### 4.3 查询生成的 Issue 列表 + +```shell script +curl -k https:///issue +``` +例如: +```shell script +curl -k https://127.0.0.1:5001/issue +``` + +### 4.4 码云查看 issue 及 PR + +登录Gitee上进行跟踪的软件项目,在该项目的Issues和Pull Requests页签下,可以查看到名为`[patch tracking] TIME`,例如` [patch tracking] 20200713101548`的条目。 + +即是刚生成的补丁文件的issue和对应PR。 + +# 五 常见问题与解决方法 + + diff --git a/patch-tracking/images/Maintainer.jpg b/patch-tracking/images/Maintainer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da0d5f1b5d928eca3a0d63795f59c55331136065 GIT binary patch literal 46243 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 X_jtE`!pkaQ>1FNXUktP#e4qF~gjd|( literal 0 HcmV?d00001 diff --git a/patch-tracking/images/PatchTracking.jpg b/patch-tracking/images/PatchTracking.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e12afd6227c18c333f289b9aa71abf608d8058a0 GIT binary patch literal 40635 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 literal 0 HcmV?d00001 diff --git a/patch-tracking/patch-tracking.spec b/patch-tracking/patch-tracking.spec new file mode 100644 index 00000000..f60fd19b --- /dev/null +++ b/patch-tracking/patch-tracking.spec @@ -0,0 +1,58 @@ +%define name patch-tracking +%define version 1.0.0 +%define release 1 + +Summary: This is a tool for automatically tracking upstream repository code patches +Name: %{name} +Version: %{version} +Release: %{release} +Source0: %{name}-%{version}.tar +License: Mulan PSL v2 +Group: Development/Libraries +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot +Prefix: %{_prefix} +BuildArch: noarch +Vendor: ChenYanpan +Url: https://openeuler.org/zh/ + +BuildRequires: python3-setuptools +# Requires: python3.7 python3-flask python3-sqlalchemy python3-requests + +%description +This is a tool for automatically tracking upstream repository code patches + +%prep +%setup -n %{name}-%{version} + +%build +%py3_build + +%install +%py3_install + +%post +sed -i "s|\blogging.conf\b|/etc/patch-tracking/logging.conf|" %{python3_sitelib}/patch_tracking/app.py +sed -i "s|\bsqlite:///db.sqlite\b|sqlite:////var/patch-tracking/db.sqlite|" %{python3_sitelib}/patch_tracking/app.py +sed -i "s|\bsettings.conf\b|/etc/patch-tracking/settings.conf|" %{python3_sitelib}/patch_tracking/app.py +chmod +x /usr/bin/patch-tracking-cli +chmod +x /usr/bin/patch-tracking +chmod +x /usr/bin/generate_password +sed -i "s|\bpatch-tracking.log\b|/var/log/patch-tracking.log|" /etc/patch-tracking/logging.conf + +%preun +%systemd_preun patch-tracking.service + +%clean +rm -rf $RPM_BUILD_ROOT + +%files +%{python3_sitelib}/* +/etc/patch-tracking/logging.conf +/etc/patch-tracking/settings.conf +/usr/bin/patch-tracking +/usr/bin/patch-tracking-cli +/var/patch-tracking/db.sqlite +/etc/patch-tracking/self-signed.crt +/etc/patch-tracking/self-signed.key +/usr/bin/generate_password +/usr/lib/systemd/system/patch-tracking.service diff --git a/patch-tracking/patch_tracking/__init__.py b/patch-tracking/patch_tracking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patch-tracking/patch_tracking/api/__init__.py b/patch-tracking/patch_tracking/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patch-tracking/patch_tracking/api/business.py b/patch-tracking/patch_tracking/api/business.py new file mode 100644 index 00000000..85041481 --- /dev/null +++ b/patch-tracking/patch_tracking/api/business.py @@ -0,0 +1,78 @@ +""" +api action method +""" +from sqlalchemy import and_ +from patch_tracking.database import db +from patch_tracking.database.models import Tracking, Issue + + +def create_tracking(data): + """ + create tracking + """ + version_control = data.get("version_control") + scm_repo = data.get('scm_repo') + scm_branch = data.get('scm_branch') + scm_commit = data.get('scm_commit') + repo = data.get('repo') + branch = data.get('branch') + enabled = data.get('enabled') + tracking = Tracking(version_control, scm_repo, scm_branch, scm_commit, repo, branch, enabled) + db.session.add(tracking) + db.session.commit() + + +def update_tracking(data): + """ + update tracking + """ + repo = data.get('repo') + branch = data.get('branch') + tracking = Tracking.query.filter(and_(Tracking.repo == repo, Tracking.branch == branch)).one() + tracking.version_control = data.get("version_control") + tracking.scm_repo = data.get('scm_repo') + tracking.scm_branch = data.get('scm_branch') + tracking.scm_commit = data.get('scm_commit') + tracking.enabled = data.get('enabled') + db.session.commit() + + +def delete_tracking(id_): + """ + delete tracking + """ + post = Tracking.query.filter(Tracking.id == id_).one() + db.session.delete(post) + db.session.commit() + + +def create_issue(data): + """ + create issue + """ + issue = data.get('issue') + repo = data.get('repo') + branch = data.get('branch') + issue_ = Issue(issue, repo, branch) + db.session.add(issue_) + db.session.commit() + + +def update_issue(data): + """ + update issue + """ + issue = data.get('issue') + issue_ = Issue.query.filter(Issue.issue == issue).one() + issue_.issue = data.get('issue') + db.session.add(issue_) + db.session.commit() + + +def delete_issue(issue): + """ + delete issue + """ + issue_ = Issue.query.filter(Issue.issue == issue).one() + db.session.delete(issue_) + db.session.commit() diff --git a/patch-tracking/patch_tracking/api/constant.py b/patch-tracking/patch_tracking/api/constant.py new file mode 100644 index 00000000..f6e19ba1 --- /dev/null +++ b/patch-tracking/patch_tracking/api/constant.py @@ -0,0 +1,50 @@ +''' + Response contain and code ID +''' +import json + + +class ResponseCode: + """ + Description: response code to web + changeLog: + """ + + SUCCESS = "2001" + INPUT_PARAMETERS_ERROR = "4001" + TRACKING_NOT_FOUND = "4002" + ISSUE_NOT_FOUND = "4003" + + GITHUB_ADDRESS_ERROR = "5001" + GITEE_ADDRESS_ERROR = "5002" + GITHUB_CONNECTION_ERROR = "5003" + GITEE_CONNECTION_ERROR = "5004" + + INSERT_DATA_ERROR = "6004" + DELETE_DB_ERROR = "6001" + CONFIGFILE_PATH_EMPTY = "6002" + DIS_CONNECTION_DB = "6003" + + CODE_MSG_MAP = { + SUCCESS: "Successful Operation!", + INPUT_PARAMETERS_ERROR: "Please enter the correct parameters", + TRACKING_NOT_FOUND: "The tracking you are looking for does not exist", + ISSUE_NOT_FOUND: "The issue you are looking for does not exist", + GITHUB_ADDRESS_ERROR: "The Github address is wrong", + GITEE_ADDRESS_ERROR: "The Gitee address is wrong", + GITHUB_CONNECTION_ERROR: "Unable to connect to the github", + GITEE_CONNECTION_ERROR: "Unable to connect to the gitee", + DELETE_DB_ERROR: "Failed to delete database", + CONFIGFILE_PATH_EMPTY: "Initialization profile does not exist or cannot be found", + DIS_CONNECTION_DB: "Unable to connect to the database, check the database configuration" + } + + @classmethod + def gen_dict(cls, code, data=None): + """ + generate response dictionary + """ + return json.dumps({"code": code, "msg": cls.CODE_MSG_MAP[code], "data": data}) + + def __str__(self): + return 'ResponseCode' diff --git a/patch-tracking/patch_tracking/api/issue.py b/patch-tracking/patch_tracking/api/issue.py new file mode 100644 index 00000000..6460698d --- /dev/null +++ b/patch-tracking/patch_tracking/api/issue.py @@ -0,0 +1,34 @@ +""" +module of issue API +""" +import logging +from flask import request +from flask import Blueprint +from patch_tracking.database.models import Issue +from patch_tracking.api.constant import ResponseCode + +log = logging.getLogger(__name__) +issue = Blueprint('issue', __name__) + + +@issue.route('', methods=["GET"]) +def get(): + """ + Returns list of issue. + """ + if not request.args: + issues = Issue.query.all() + else: + required_params = ['repo', 'branch'] + input_params = request.args + data = dict() + for k, param in input_params.items(): + if k in required_params: + data[k] = param + else: + return ResponseCode.gen_dict(ResponseCode.INPUT_PARAMETERS_ERROR) + issues = Issue.query.filter_by(**data).all() + resp_data = list() + for item in issues: + resp_data.append(item.to_json()) + return ResponseCode.gen_dict(code=ResponseCode.SUCCESS, data=resp_data) diff --git a/patch-tracking/patch_tracking/api/tracking.py b/patch-tracking/patch_tracking/api/tracking.py new file mode 100644 index 00000000..1318abd2 --- /dev/null +++ b/patch-tracking/patch_tracking/api/tracking.py @@ -0,0 +1,83 @@ +""" +module of issue API +""" +import logging +from flask import request, Blueprint +from patch_tracking.database.models import Tracking +from patch_tracking.api.business import create_tracking, update_tracking +from patch_tracking.api.constant import ResponseCode +from patch_tracking.util.auth import auth + +logger = logging.getLogger(__name__) +tracking = Blueprint('tracking', __name__) + + +@tracking.route('', methods=["GET"]) +def get(): + """ + Returns list of tracking + """ + if not request.args: + trackings = Tracking.query.all() + else: + required_params = ['repo', 'branch', 'enabled'] + input_params = request.args + data = dict() + for k, param in input_params.items(): + if k in required_params: + if k == 'enabled': + param = bool(param == 'true') + data[k] = param + required_params.remove(k) + else: + return ResponseCode.gen_dict(ResponseCode.INPUT_PARAMETERS_ERROR) + + if 'repo' in required_params and 'branch' not in required_params: + return ResponseCode.gen_dict(ResponseCode.INPUT_PARAMETERS_ERROR) + + trackings = Tracking.query.filter_by(**data).all() + + resp_data = list() + for item in trackings: + resp_data.append(item.to_json()) + return ResponseCode.gen_dict(code=ResponseCode.SUCCESS, data=resp_data) + + +@tracking.route('', methods=["POST"]) +@auth.login_required +def post(): + """ + Creates os update a tracking. + """ + required_params = ['version_control', 'scm_repo', 'scm_branch', 'scm_commit', 'repo', 'branch', 'enabled'] + input_params = request.json + data = dict() + for item in input_params: + if item in required_params: + data[item] = input_params[item] + required_params.remove(item) + else: + return ResponseCode.gen_dict(ResponseCode.INPUT_PARAMETERS_ERROR) + + if required_params: + if len(required_params) == 1 and required_params[0] == 'scm_commit': + pass + else: + return ResponseCode.gen_dict(ResponseCode.INPUT_PARAMETERS_ERROR) + if data['version_control'] != 'github': + return ResponseCode.gen_dict(ResponseCode.INPUT_PARAMETERS_ERROR) + + track = Tracking.query.filter_by(repo=data['repo'], branch=data['branch']).first() + if track: + try: + update_tracking(data) + logger.info('Update tracking. Data: %s.', data) + except Exception as exception: + return ResponseCode.gen_dict(code=ResponseCode.INSERT_DATA_ERROR, data=exception) + else: + try: + create_tracking(data) + logger.info('Create tracking. Data: %s.', data) + except Exception as exception: + return ResponseCode.gen_dict(code=ResponseCode.INSERT_DATA_ERROR, data=exception) + return ResponseCode.gen_dict(code=ResponseCode.SUCCESS, data=request.json) diff --git a/patch-tracking/patch_tracking/app.py b/patch-tracking/patch_tracking/app.py new file mode 100644 index 00000000..e8abdec8 --- /dev/null +++ b/patch-tracking/patch_tracking/app.py @@ -0,0 +1,59 @@ +""" +flask app +""" +import logging.config +import sys +from flask import Flask +from patch_tracking.api.issue import issue +from patch_tracking.api.tracking import tracking +from patch_tracking.database import db +from patch_tracking.task import task + +logging.config.fileConfig('logging.conf', disable_existing_loggers=False) + +app = Flask(__name__) +logger = logging.getLogger(__name__) + +app.config.from_pyfile("settings.conf") + + +def check_settings_conf(): + """ + check settings.conf + """ + flag = 0 + required_settings = ['LISTEN', 'GITHUB_ACCESS_TOKEN', 'GITEE_ACCESS_TOKEN', 'SCAN_DB_INTERVAL', 'USER', 'PASSWORD'] + for setting in required_settings: + if setting in app.config: + if not app.config[setting]: + logger.error('%s is empty in settings.conf.', setting) + flag = 1 + else: + logger.error('%s not configured in settings.conf.', setting) + flag = 1 + if flag: + sys.exit() + + +check_settings_conf() + +GITHUB_ACCESS_TOKEN = app.config['GITHUB_ACCESS_TOKEN'] +GITEE_ACCESS_TOKEN = app.config['GITEE_ACCESS_TOKEN'] +SCAN_DB_INTERVAL = app.config['SCAN_DB_INTERVAL'] + +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite?check_same_thread=False' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SWAGGER_UI_DOC_EXPANSION'] = 'list' +app.config['ERROR_404_HELP'] = False +app.config['RESTX_MASK_SWAGGER'] = False +app.config['SCHEDULER_EXECUTORS'] = {'default': {'type': 'threadpool', 'max_workers': 100}} + +app.register_blueprint(issue, url_prefix="/issue") +app.register_blueprint(tracking, url_prefix="/tracking") + +db.init_app(app) + +task.job_init(app) + +if __name__ == "__main__": + app.run(ssl_context="adhoc") diff --git a/patch-tracking/patch_tracking/cli/__init__.py b/patch-tracking/patch_tracking/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patch-tracking/patch_tracking/cli/generate_password b/patch-tracking/patch_tracking/cli/generate_password new file mode 100644 index 00000000..9cb861b0 --- /dev/null +++ b/patch-tracking/patch_tracking/cli/generate_password @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +command line to generate password hash by pbkdf2 +""" + +import sys +import re +from werkzeug.security import generate_password_hash + + +def password_strength_check(password): + """ + Verify the strength of 'password' + Returns a dict indicating the wrong criteria + """ + + # calculating the length + length_error = len(password) < 6 + + # searching for digits + digit_error = re.search(r"\d", password) is None + + # searching for uppercase + uppercase_error = re.search(r"[A-Z]", password) is None + + # searching for lowercase + lowercase_error = re.search(r"[a-z]", password) is None + + # searching for symbols + symbol_error = re.search(r"[~!@#%^*_+=-]", password) is None + + # overall result + password_ok = not (length_error or digit_error or uppercase_error or lowercase_error or symbol_error) + + return { + 'ok': password_ok, + 'error': { + 'length': length_error, + 'digit': digit_error, + 'uppercase': uppercase_error, + 'lowercase': lowercase_error, + 'symbol': symbol_error, + } + } + + +ret = password_strength_check(sys.argv[1]) +if not ret['ok']: + print("Password strength is not satisfied.") + for item in ret['error']: + if ret['error'][item]: + print("{} not satisfied.".format(item)) + print( + """ +password strength require: + 6 characters or more + at least 1 digit [0-9] + at least 1 alphabet [a-z] + at least 1 alphabet of Upper Case [A-Z] + at least 1 special character from [~!@#%^*_+=-] +""" + ) +else: + print(generate_password_hash(sys.argv[1])) diff --git a/patch-tracking/patch_tracking/cli/patch-tracking-cli b/patch-tracking/patch_tracking/cli/patch-tracking-cli new file mode 100644 index 00000000..bf88f93a --- /dev/null +++ b/patch-tracking/patch_tracking/cli/patch-tracking-cli @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +# -*- coding: utf-8 -*- +import re +import sys + +from patch_tracking.cli.patch_tracking_cli import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/patch-tracking/patch_tracking/cli/patch_tracking_cli.py b/patch-tracking/patch_tracking/cli/patch_tracking_cli.py new file mode 100755 index 00000000..2dc499a8 --- /dev/null +++ b/patch-tracking/patch_tracking/cli/patch_tracking_cli.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +command line of creating tracking item +""" +import argparse +import sys +import os +import requests +from requests.auth import HTTPBasicAuth +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +USAGE = """ + patch-tracking-cli --help + patch-tracking-cli --server SERVER --version_control github --scm_repo SCM_REPO --scm_branch SCM_BRANCH \\ + --repo REPO --branch BRANCH --enabled True --user USER --password PWD + patch-tracking-cli --server SERVER --file FILE --user USER --password PWD + patch-tracking-cli --server SERVER --dir DIR --user USER --password PWD +""" + +parser = argparse.ArgumentParser( + usage=USAGE, allow_abbrev=False, description="command line to create/update patch tracking item" +) + +parser.add_argument("--server", help="patch tracking daemon server") + +parser.add_argument("--version_control", choices=['github'], help="upstream version control system") +parser.add_argument("--scm_repo", help="upstream scm repository") +parser.add_argument("--scm_branch", help="upstream scm branch") +parser.add_argument("--repo", help="source package repository") +parser.add_argument("--branch", help="source package branch") +parser.add_argument("--enabled", choices=["True", "true", "False", "false"], help="whether tracing is enabled") + +parser.add_argument('--file', help='import patch tracking from file') + +parser.add_argument('--dir', help='import patch tracking from files in directory') +parser.add_argument('--user', help='Authentication username') +parser.add_argument('--password', help='Authentication password') + +args = parser.parse_args() + +style1 = args.version_control or args.repo or args.branch or args.scm_repo or args.scm_branch or args.enabled +style2 = bool(args.file) +style3 = bool(args.dir) + +if str([style1, style2, style3]).count('True') >= 2: + print("mix different usage style") + parser.print_usage() + sys.exit(-1) + + +def single_input_track(params, file_path=None): + """ + load tracking from ommand lcine arguments + """ + if param_check(params, file_path) == 'error': + return 'error', 'Check input params error.' + if param_check_url(params, file_path) == 'error': + return 'error', 'Check input params error.' + + repo = params['repo'] + branch = params['branch'] + scm_repo = params['scm_repo'] + scm_branch = params['scm_branch'] + version_control = params['version_control'].lower() + enabled = params['enabled'].lower() + server = params['server'] + user = params['user'] + password = params['password'] + + enabled = bool(enabled == 'true') + + url = '/'.join(['https:/', server, 'tracking']) + data = { + 'version_control': version_control, + 'scm_repo': scm_repo, + 'scm_branch': scm_branch, + 'repo': repo, + 'branch': branch, + 'enabled': enabled + } + try: + ret = requests.post(url, json=data, verify=False, auth=HTTPBasicAuth(user, password)) + except Exception as exception: + return 'error', 'Connect server error: ' + str(exception) + if ret.status_code == 401 or ret.status_code == 403: + return 'error', 'Authenticate Error. Please make sure user and password are correct.' + if ret.status_code == 200 and ret.json()['code'] == '2001': + return 'success', 'created' + else: + print("status_code: {}, return text: {}".format(ret.status_code, ret.text)) + return 'error', 'Unexpected Error.' + + +def file_input_track(file_path): + """ + load tracking from file + """ + if os.path.exists(file_path) and os.path.isfile(file_path): + if os.path.splitext(file_path)[-1] != ".yaml": + print('Please input yaml file. Error in {}'.format(file_path)) + return None + with open(file_path) as file: + content = file.readlines() + params = dict() + for item in content: + if ":" in item: + k = item.split(':')[0] + value = item.split(':')[1].strip(' ').strip('\n') + params.update({k: value}) + params.update({'server': args.server, 'user': args.user, 'password': args.password}) + ret = single_input_track(params, file_path) + if ret[0] == 'success': + print('Tracking successfully {} for {}'.format(ret[1], file_path)) + else: + print('Tracking failed for {}: {}'.format(file_path, ret[1])) + else: + print('yaml path error. Params error in {}'.format(file_path)) + + +def dir_input_track(dir_path): + """ + load tracking from dir + """ + if os.path.exists(dir_path) and os.path.isdir(dir_path): + for root, _, files in os.walk(dir_path): + if not files: + print('error: dir path empty') + return None + for file in files: + if os.path.splitext(file)[-1] == ".yaml": + file_path = os.path.join(root, file) + file_input_track(file_path) + else: + print('Please input yaml file. Error in {}'.format(file)) + else: + print('error: dir path error. Params error in {}'.format(dir_path)) + + +def patch_tracking_server_check(url): + """ + check if patch_tracking server start + """ + try: + ret = requests.head(url=url, verify=False) + except Exception as exception: + print(f"Error: Cannot connect to {url}, please make sure patch-tracking service is running.") + return 'error', exception + if ret.status_code == 200 or ret.status_code == 404: + return 'success', ret + + print(f"Unexpected Error: {ret.text}") + return 'error', ret.text + + +def repo_branch_check(url): + """ + check if repo/branch exist + """ + headers = { + "User-Agent": + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + + "Ubuntu Chromium/83.0.4103.61 Chrome/83.0.4103.61 Safari/537.36" + } + try: + ret = requests.get(url=url, headers=headers) + except Exception as exception: + return 'error', exception + if ret.status_code == 404: + return 'error', f'{url} not exist.' + if ret.status_code == 200: + return 'success', ret + + return 'error', ret.text + + +def command_default_param_check(): + flag = 0 + if not args.server: + print("Error: --server not configure.") + flag = 1 + if not args.user: + print("Error: --user not configure.") + flag = 1 + if not args.password: + print("Error: --password not configure.") + flag = 1 + if flag == 1: + return 'error' + else: + return 'success' + + +def param_check(params, file_path=None): + """ + check if param is valid + """ + flag = 0 + required_param = ['version_control', 'scm_repo', 'scm_branch', 'repo', 'branch', 'enabled', 'user', 'password'] + for req in required_param: + if req not in params: + if file_path: + print(f'param: --{req} must be configured. Error in {file_path}') + else: + print(f'param: --{req} must be configured.') + flag = 1 + for k, value in params.items(): + if not value: + if file_path: + print(f'param: --{k} must be configured. Error in {file_path}') + else: + print(f'param: --{k} cannot be empty.') + flag = 1 + if flag: + return 'error' + return None + + +def param_check_url(params, file_path=None): + """ + check url + """ + scm_url = f"https://github.com/{params['scm_repo']}/tree/{params['scm_branch']}" + url = f"https://gitee.com/{params['repo']}/tree/{params['branch']}" + patch_tracking_url = f"https://{params['server']}" + server_ret = patch_tracking_server_check(patch_tracking_url) + if server_ret[0] != 'success': + return 'error' + + scm_ret = repo_branch_check(scm_url) + if scm_ret[0] != 'success': + if file_path: + print( + f"scm_repo: {params['scm_repo']} and scm_branch: {params['scm_branch']} check failed. \n" + f"Error in {file_path}. {scm_ret[1]}" + ) + else: + print(f"scm_repo: {params['scm_repo']} and scm_branch: {params['scm_branch']} check failed. {scm_ret[1]}") + return 'error' + ret = repo_branch_check(url) + if ret[0] != 'success': + if file_path: + print(f"repo: {params['repo']} and branch: {params['branch']} check failed. {ret[1]}. Error in {file_path}") + else: + print(f"repo: {params['repo']} and branch: {params['branch']} check failed. {ret[1]}.") + return 'error' + return None + + +def main(): + """ + main + """ + + if command_default_param_check() == 'error': + return None + + if style2: + file_input_track(args.file) + elif style3: + dir_input_track(args.dir) + else: + params = { + 'repo': args.repo, + 'branch': args.branch, + 'scm_repo': args.scm_repo, + 'scm_branch': args.scm_branch, + 'version_control': args.version_control, + 'enabled': args.enabled, + 'server': args.server, + 'user': args.user, + 'password': args.password + } + ret = single_input_track(params) + if ret[0] == 'success': + print('Tracking successfully.') + else: + print(ret[1]) + + +if __name__ == '__main__': + main() diff --git a/patch-tracking/patch_tracking/database/__init__.py b/patch-tracking/patch_tracking/database/__init__.py new file mode 100644 index 00000000..83b427ca --- /dev/null +++ b/patch-tracking/patch_tracking/database/__init__.py @@ -0,0 +1,14 @@ +""" +database init +""" +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +def reset_database(): + """ + reset database + """ + db.drop_all() + db.create_all() diff --git a/patch-tracking/patch_tracking/database/models.py b/patch-tracking/patch_tracking/database/models.py new file mode 100644 index 00000000..8aee57cd --- /dev/null +++ b/patch-tracking/patch_tracking/database/models.py @@ -0,0 +1,67 @@ +""" +module of database model +""" +from patch_tracking.database import db + + +class Tracking(db.Model): + """ + database model of tracking + """ + id = db.Column(db.Integer, autoincrement=True) + version_control = db.Column(db.String(80)) + scm_repo = db.Column(db.String(80)) + scm_branch = db.Column(db.String(80)) + scm_commit = db.Column(db.String(80)) + repo = db.Column(db.String(80), primary_key=True) + branch = db.Column(db.String(80), primary_key=True) + enabled = db.Column(db.Boolean) + + def __init__(self, version_control, scm_repo, scm_branch, scm_commit, repo, branch, enabled=True): + self.version_control = version_control + self.scm_repo = scm_repo + self.scm_branch = scm_branch + self.scm_commit = scm_commit + self.repo = repo + self.branch = branch + self.enabled = enabled + + def __repr__(self): + return '' % (self.repo, self.branch) + + def to_json(self): + """ + convert to json + """ + return { + 'version_control': self.version_control, + 'scm_repo': self.scm_repo, + 'scm_branch': self.scm_branch, + 'scm_commit': self.scm_commit, + 'repo': self.repo, + 'branch': self.branch, + 'enabled': self.enabled + } + + +class Issue(db.Model): + """ + database model of issue + """ + issue = db.Column(db.String(80), primary_key=True) + repo = db.Column(db.String(80)) + branch = db.Column(db.String(80)) + + def __init__(self, issue, repo, branch): + self.issue = issue + self.repo = repo + self.branch = branch + + def __repr__(self): + return '' % (self.issue, self.repo, self.branch) + + def to_json(self): + """ + convert to json + """ + return {'issue': self.issue, 'repo': self.repo, 'branch': self.branch} diff --git a/patch-tracking/patch_tracking/database/reset_db.py b/patch-tracking/patch_tracking/database/reset_db.py new file mode 100644 index 00000000..7581dea5 --- /dev/null +++ b/patch-tracking/patch_tracking/database/reset_db.py @@ -0,0 +1,17 @@ +""" +reset database +""" +from patch_tracking.app import app +from patch_tracking.database import reset_database + + +def reset(): + """ + reset database + """ + with app.app_context(): + reset_database() + + +if __name__ == "__main__": + reset() diff --git a/patch-tracking/patch_tracking/db.sqlite b/patch-tracking/patch_tracking/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..aa4d6cc3dc7000855b726c6e0300b4cb556f13f1 GIT binary patch literal 20480 zcmeI%!ET!{7{GDUrtON9wl_|Ze1s|58g0^cnN*5U8E0hrHoZ(70R#|0009ILKmY**5I|s! zKz*kAy(Zj*ZuQ**!!WelvWh0NBrVIEMbU8r*B-laY@d6sY~s??4;PIOcHmsvf%)>( zlKyBc{r8^Nm-=Cme9h&9P8FLJQ5Jt*jlKznukGMkUb)xOYzDP-%j)bKuMAD3*);i4 zew)?D%`m#D^5!{g-Uz$?9x6vknsT}%a_reX- zE(WnwuXE`-SJG_18~V~b?aP5xBR}2QH=N!k None: + ''' + Prepare the environment + :return: + ''' + self.client = app.test_client() + reset_db.reset() + + def test_none_data(self): + ''' + In the absence of data, the GET interface queries all the data + :return: + ''' + with app.app_context(): + + resp = self.client.get("/issue") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertEqual(resp_dict.get("data"), [], msg="Error in data information return") + + def test_query_inserted_data(self): + ''' + The GET interface queries existing data + :return: + ''' + with app.app_context(): + data_insert = {"issue": "A", "repo": "A", "branch": "A"} + + create_issue(data_insert) + + resp = self.client.get("/issue?repo=A&branch=A") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertIn(data_insert, resp_dict.get("data"), msg="Error in data information return") + + def test_find_all_data(self): + ''' + The GET interface queries all the data + :return: + ''' + with app.app_context(): + data_insert_c = {"issue": "C", "repo": "C", "branch": "C"} + data_insert_d = {"issue": "D", "repo": "D", "branch": "D"} + create_issue(data_insert_c) + create_issue(data_insert_d) + resp = self.client.get("/issue") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertIn(data_insert_c, resp_dict.get("data"), msg="Error in data information return") + self.assertIn(data_insert_d, resp_dict.get("data"), msg="Error in data information return") + + def test_find_nonexistent_data(self): + ''' + The GET interface queries data that does not exist + :return: + ''' + with app.app_context(): + + resp = self.client.get("/issue?repo=aa&branch=aa") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertEqual(resp_dict.get("data"), [], msg="Error in data information return") + + def test_get_error_parameters(self): + ''' + The get interface passes in the wrong parameter + :return: + ''' + with app.app_context(): + data_insert = {"issue": "BB", "repo": "BB", "branch": "BB"} + + create_issue(data_insert) + + resp = self.client.get("/issue?oper=BB&chcnsrb=BB") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return" + ) + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_get_interface_uppercase(self): + ''' + The get interface uppercase + :return: + ''' + with app.app_context(): + data_insert = {"issue": "CCC", "repo": "CCC", "branch": "CCC"} + + create_issue(data_insert) + + resp = self.client.get("/issue?RrPo=CCC&brANch=CCC") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return" + ) + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + +if __name__ == '__main__': + unittest.main() diff --git a/patch-tracking/patch_tracking/tests/logging.conf b/patch-tracking/patch_tracking/tests/logging.conf new file mode 100644 index 00000000..f153c42f --- /dev/null +++ b/patch-tracking/patch_tracking/tests/logging.conf @@ -0,0 +1,22 @@ +[loggers] +keys=root + +[handlers] +keys=console + +[formatters] +keys=simple + +[logger_root] +level=DEBUG +handlers=console + +[handler_console] +class=StreamHandler +level=DEBUG +formatter=simple +args=(sys.stdout,) + +[formatter_simple] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= diff --git a/patch-tracking/patch_tracking/tests/tracking_test.py b/patch-tracking/patch_tracking/tests/tracking_test.py new file mode 100644 index 00000000..cb7cf185 --- /dev/null +++ b/patch-tracking/patch_tracking/tests/tracking_test.py @@ -0,0 +1,400 @@ +# -*- coding:utf-8 -*- +''' +Automated testing of the Tracking interface, including POST requests and GET requests +''' +import unittest +import json +from base64 import b64encode +from werkzeug.security import generate_password_hash +from patch_tracking.app import app +from patch_tracking.database import reset_db +from patch_tracking.api.business import create_tracking +from patch_tracking.api.constant import ResponseCode + + +class TestTracking(unittest.TestCase): + ''' + Automated testing of the Tracking interface, including POST requests and GET requests + ''' + def setUp(self) -> None: + ''' + Prepare the environment + :return: + ''' + self.client = app.test_client() + reset_db.reset() + app.config["USER"] = "hello" + app.config["PASSWORD"] = generate_password_hash("world") + + credentials = b64encode(b"hello:world").decode('utf-8') + self.auth = {"Authorization": f"Basic {credentials}"} + + def test_none_data(self): + ''' + In the absence of data, the GET interface queries all the data + :return: + ''' + with app.app_context(): + + resp = self.client.get("/tracking") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertEqual(resp_dict.get("data"), [], msg="Error in data information return") + + def test_find_nonexistent_data(self): + ''' + The GET interface queries data that does not exist + :return: + ''' + with app.app_context(): + + resp = self.client.get("/tracking?repo=aa&branch=aa") + + resp_dict = json.loads(resp.data) + + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertEqual(resp_dict.get("data"), [], msg="Error in data information return") + + def test_insert_data(self): + ''' + The POST interface inserts data + :return: + ''' + data = { + "version_control": "github", + "scm_repo": "A", + "scm_branch": "A", + "scm_commit": "A", + "repo": "A", + "branch": "A", + "enabled": 0 + } + + resp = self.client.post("/tracking", json=data, content_type="application/json", headers=self.auth) + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + + def test_query_inserted_data(self): + ''' + The GET interface queries existing data + :return: + ''' + with app.app_context(): + data_insert = { + "version_control": "github", + "scm_repo": "B", + "scm_branch": "B", + "scm_commit": "B", + "repo": "B", + "branch": "B", + "enabled": False + } + + create_tracking(data_insert) + + resp = self.client.get("/tracking?repo=B&branch=B") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + self.assertIn(data_insert, resp_dict.get("data"), msg="Error in data information return") + + def test_only_input_branch(self): + ''' + Get interface queries enter only BRANCH, not REPO + :return: + ''' + with app.app_context(): + data_insert = { + "version_control": "github", + "scm_repo": "C", + "scm_branch": "C", + "scm_commit": "C", + "repo": "C", + "branch": "C", + "enabled": 0 + } + + create_tracking(data_insert) + + resp = self.client.get("/tracking?branch=B") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return" + ) + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_fewer_parameters(self): + ''' + When the POST interface passes in parameters, fewer parameters must be passed + :return: + ''' + data = {"version_control": "github", "scm_commit": "AA", "repo": "AA", "branch": "AA", "enabled": 1} + + resp = self.client.post("/tracking", json=data, content_type="application/json", headers=self.auth) + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_error_parameters_value(self): + ''' + The post interface passes in the wrong parameter + :return: + ''' + data = {"version_control": "github", "scm_commit": "AA", "repo": "AA", "branch": "AA", "enabled": "AA"} + + resp = self.client.post("/tracking", json=data, content_type="application/json", headers=self.auth) + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_post_error_parameters(self): + ''' + The post interface passes in the wrong parameter + :return: + ''' + data = {"version_control": "github", "scm_commit": "AA", "oper": "AA", "hcnarb": "AA", "enabled": "AA"} + + resp = self.client.post("/tracking", json=data, content_type="application/json", headers=self.auth) + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_get_error_parameters(self): + ''' + The get interface passes in the wrong parameter + :return: + ''' + with app.app_context(): + data_insert = { + "version_control": "github", + "scm_repo": "BB", + "scm_branch": "BB", + "scm_commit": "BB", + "repo": "BB", + "branch": "BB", + "enabled": True + } + + create_tracking(data_insert) + + resp = self.client.get("/tracking?oper=B&chcnsrb=B") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return" + ) + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_update_data(self): + ''' + update data + :return: + ''' + with app.app_context(): + data_old = { + "version_control": "github", + "scm_repo": "str", + "scm_branch": "str", + "scm_commit": "str", + "repo": "string", + "branch": "string", + "enabled": False + } + + self.client.post("/tracking", json=data_old, content_type="application/json", headers=self.auth) + + data_new = { + "branch": "string", + "enabled": True, + "repo": "string", + "scm_branch": "string", + "scm_commit": "string", + "scm_repo": "string", + "version_control": "github", + } + + self.client.post("/tracking", json=data_new, content_type="application/json") + + resp = self.client.get("/tracking?repo=string&branch=string") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.SUCCESS, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.SUCCESS), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertIsNotNone(resp_dict.get("data"), msg="Error in data information return") + #self.assertIn(data_new, resp_dict.get("data"), msg="Error in data information return") + + def test_get_interface_uppercase(self): + ''' + The get interface uppercase + :return: + ''' + with app.app_context(): + data_insert = { + "version_control": "github", + "scm_repo": "BBB", + "scm_branch": "BBB", + "scm_commit": "BBB", + "repo": "BBB", + "branch": "BBB", + "enabled": False + } + + create_tracking(data_insert) + + resp = self.client.get("/tracking?rep=BBB&BRAnch=BBB") + + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return" + ) + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + def test_version_control_error(self): + ''' + The POST version control error + :return: + ''' + data = { + "version_control": "gitgitgit", + "scm_repo": "A", + "scm_branch": "A", + "scm_commit": "A", + "repo": "A", + "branch": "A", + "enabled": 0 + } + + resp = self.client.post("/tracking", json=data, content_type="application/json", headers=self.auth) + resp_dict = json.loads(resp.data) + self.assertIn("code", resp_dict, msg="Error in data format return") + self.assertEqual(ResponseCode.INPUT_PARAMETERS_ERROR, resp_dict.get("code"), msg="Error in status code return") + + self.assertIn("msg", resp_dict, msg="Error in data format return") + self.assertEqual( + ResponseCode.CODE_MSG_MAP.get(ResponseCode.INPUT_PARAMETERS_ERROR), + resp_dict.get("msg"), + msg="Error in status code return" + ) + + self.assertIn("data", resp_dict, msg="Error in data format return") + self.assertEqual(resp_dict.get("data"), None, msg="Error in data information return") + + +if __name__ == '__main__': + unittest.main() diff --git a/patch-tracking/patch_tracking/util/__init__.py b/patch-tracking/patch_tracking/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patch-tracking/patch_tracking/util/auth.py b/patch-tracking/patch_tracking/util/auth.py new file mode 100644 index 00000000..df9ed248 --- /dev/null +++ b/patch-tracking/patch_tracking/util/auth.py @@ -0,0 +1,19 @@ +""" +http basic auth +""" +from werkzeug.security import check_password_hash +from flask_httpauth import HTTPBasicAuth +from flask import current_app as app + +auth = HTTPBasicAuth() + + +@auth.verify_password +def verify_password(username, password): + """ + verify password + """ + if username == app.config["USER"] and \ + check_password_hash(app.config["PASSWORD"], password): + return username + return None diff --git a/patch-tracking/patch_tracking/util/gitee_api.py b/patch-tracking/patch_tracking/util/gitee_api.py new file mode 100644 index 00000000..025637ed --- /dev/null +++ b/patch-tracking/patch_tracking/util/gitee_api.py @@ -0,0 +1,137 @@ +""" +function of invoking Gitee API +""" +import base64 +import logging +import requests +from flask import current_app + +log = logging.getLogger(__name__) + +ORG_URL = "https://gitee.com/api/v5/orgs" +REPO_URL = "https://gitee.com/api/v5/repos" + + +def get_path_content(repo, branch, path): + """ + get file content + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + url = '/'.join([REPO_URL, repo, 'contents', path]) + param = {'access_token': gitee_token, 'ref': branch} + ret = requests.get(url, params=param).json() + return ret + + +def post_create_branch(repo, branch, new_branch): + """ + create branch + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + url = '/'.join([REPO_URL, repo, 'branches']) + data = {'access_token': gitee_token, 'refs': branch, 'branch_name': new_branch} + response = requests.post(url, data=data) + if response.status_code == 201: + return 'success' + + return response.json() + + +def post_upload_patch(data): + """ + upload patch + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + patch_file_name = data['latest_commit_id'] + '.patch' + url = '/'.join([REPO_URL, data['repo'], 'contents', patch_file_name]) + content = base64.b64encode(data['patch_file_content'].encode("utf-8")) + message = '[patch tracking] ' + data['cur_time'] + ' - ' + data['commit_url'] + '\n' + data = {'access_token': gitee_token, 'content': content, 'message': message, 'branch': data['branch']} + response = requests.post(url, data=data) + if response.status_code == 201: + return 'success' + + return response.json() + + +def post_create_spec(repo, branch, spec_content, cur_time): + """ + create spec + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + owner, repo = repo.split('/') + spec_file_name = repo + '.spec' + url = '/'.join([REPO_URL, owner, repo, 'contents', spec_file_name]) + content = base64.b64encode(spec_content.encode("utf-8")) + message = '[patch tracking] ' + cur_time + ' - ' + 'create spec file' + '\n' + data = {'access_token': gitee_token, 'content': content, 'message': message, 'branch': branch} + response = requests.post(url, data=data) + if response.status_code == 201: + return 'success' + + return response.json() + + +def put_upload_spec(repo, branch, cur_time, spec_content, spec_sha): + """ + upload spec + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + owner, repo = repo.split('/') + spec_file_name = repo + '.spec' + url = '/'.join([REPO_URL, owner, repo, 'contents', spec_file_name]) + content = base64.b64encode(spec_content.encode("utf-8")) + message = '[patch tracking] ' + cur_time + ' - ' + 'update spec file' + '\n' + data = { + 'access_token': gitee_token, + 'owner': owner, + 'repo': repo, + 'path': spec_file_name, + 'content': content, + 'message': message, + 'branch': branch, + 'sha': spec_sha + } + response = requests.put(url, data=data) + if response.status_code == 200: + return 'success' + + return response.json() + + +def post_create_issue(repo, issue_body, cur_time): + """ + create issue + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + owner, repo = repo.split('/') + url = '/'.join([REPO_URL, owner, 'issues']) + data = {'access_token': gitee_token, 'repo': repo, 'title': '[patch tracking] ' + cur_time, 'body': issue_body} + response = requests.post(url, data=data) + if response.status_code == 201: + return 'success', response.json()['number'] + + return 'error', response.json() + + +def post_create_pull_request(repo, branch, patch_branch, issue_num, cur_time): + """ + create pull request + """ + gitee_token = current_app.config['GITEE_ACCESS_TOKEN'] + owner, repo = repo.split('/') + url = '/'.join([REPO_URL, owner, repo, 'pulls']) + data = { + 'access_token': gitee_token, + 'repo': repo, + 'title': '[patch tracking] ' + cur_time, + 'head': patch_branch, + 'base': branch, + 'body': '#' + issue_num, + "prune_source_branch": "true" + } + response = requests.post(url, data=data) + if response.status_code == 201: + return 'success' + + return response.json() diff --git a/patch-tracking/patch_tracking/util/github_api.py b/patch-tracking/patch_tracking/util/github_api.py new file mode 100644 index 00000000..c2196c5b --- /dev/null +++ b/patch-tracking/patch_tracking/util/github_api.py @@ -0,0 +1,118 @@ +""" +functionality of invoking GitHub API +""" +import time +import logging +import requests +from requests.exceptions import ConnectionError as requests_connectionError +from flask import current_app + +logger = logging.getLogger(__name__) + + +class GitHubApi: + """ + Encapsulates GitHub functionality + """ + def __init__(self): + github_token = current_app.config['GITHUB_ACCESS_TOKEN'] + token = 'token ' + github_token + self.headers = { + 'User-Agent': 'Mozilla/5.0', + 'Authorization': token, + 'Content-Type': 'application/json', + 'Connection': 'close', + 'method': 'GET', + 'Accept': 'application/json' + } + + def api_request(self, url): + """ + request GitHub API + """ + logger.debug("Connect url: %s", url) + count = 30 + while count > 0: + try: + response = requests.get(url, headers=self.headers) + return response + except requests_connectionError as err: + logger.warning(err) + time.sleep(10) + count -= 1 + continue + if count == 0: + logger.error('Fail to connnect to github: %s after retry 30 times.', url) + return 'connect error' + + def get_commit_info(self, repo_url, commit_id): + """ + get commit info + """ + res_dict = dict() + api_url = 'https://api.github.com/repos' + url = '/'.join([api_url, repo_url, 'commits', commit_id]) + ret = self.api_request(url) + if ret != 'connect error': + if ret.status_code == 200: + res_dict['commit_id'] = commit_id + res_dict['message'] = ret.json()['commit']['message'] + res_dict['time'] = ret.json()['commit']['author']['date'] + if 'parents' in ret.json() and ret.json()['parents']: + res_dict['parent'] = ret.json()['parents'][0]['sha'] + return 'success', res_dict + + logger.error('%s failed. Return val: %s', url, ret) + return 'error', ret.json() + return 'error', 'connect error' + + def get_latest_commit(self, repo_url, branch): + """ + get latest commit_ID, commit_message, commit_date + :param repo_url: + :param branch: + :return: res_dict + """ + api_url = 'https://api.github.com/repos' + url = '/'.join([api_url, repo_url, 'branches', branch]) + ret = self.api_request(url) + res_dict = dict() + if ret != 'connect error': + if ret.status_code == 200: + res_dict['latest_commit'] = ret.json()['commit']['sha'] + res_dict['message'] = ret.json()['commit']['commit']['message'] + res_dict['time'] = ret.json()['commit']['commit']['committer']['date'] + return 'success', res_dict + + logger.error('%s failed. Return val: %s', url, ret) + return 'error', ret.json() + + return 'error', 'connect error' + + def get_patch(self, repo_url, scm_commit, last_commit): + """ + get patch + """ + api_url = 'https://github.com' + if scm_commit != last_commit: + commit = scm_commit + '...' + last_commit + '.diff' + else: + commit = scm_commit + '^...' + scm_commit + '.diff' + ret_dict = dict() + + url = '/'.join([api_url, repo_url, 'compare', commit]) + ret = self.api_request(url) + if ret != 'connect error': + if ret.status_code == 200: + patch_content = ret.text + ret_dict['status'] = 'success' + ret_dict['api_ret'] = patch_content + else: + logger.error('%s failed. Return val: %s', url, ret) + ret_dict['status'] = 'error' + ret_dict['api_ret'] = ret.text + else: + ret_dict['status'] = 'error' + ret_dict['api_ret'] = 'fail to connect github by api.' + + return ret_dict diff --git a/patch-tracking/patch_tracking/util/spec.py b/patch-tracking/patch_tracking/util/spec.py new file mode 100644 index 00000000..84f6b9d2 --- /dev/null +++ b/patch-tracking/patch_tracking/util/spec.py @@ -0,0 +1,121 @@ +""" +functionality of modify the spec file +""" + +import re + + +class Spec: + """ + functionality of update spec file + """ + def __init__(self, content): + self._lines = content.splitlines() + self.version = "0.0" + self.release = {"num": 0, "lineno": 0} + self.source_lineno = 0 + self.patch = {"threshold": 6000, "max_num": 0, "lineno": 0} + self.changelog_lineno = 0 + + # 规避空文件异常 + if len(self._lines) == 0: + self._lines.append("") + + # 查找配置项最后一次出现所在行的行号 + for i, line in enumerate(self._lines): + match_find = re.match(r"[ \t]*Version:[ \t]*([\d.]+)", line) + if match_find: + self.version = match_find[1] + continue + + match_find = re.match(r"[ \t]*Release:[ \t]*([\d.]+)", line) + if match_find: + self.release["num"] = int(match_find[1]) + self.release["lineno"] = i + continue + + match_find = re.match(r"[ \t]*%changelog", line) + if match_find: + self.changelog_lineno = i + continue + + match_find = re.match(r"[ \t]*Source([\d]*):", line) + if match_find: + self.source_lineno = i + continue + + match_find = re.match(r"[ \t]*Patch([\d]+):", line) + if match_find: + num = int(match_find[1]) + self.patch["lineno"] = 0 + if num > self.patch["max_num"]: + self.patch["max_num"] = num + self.patch["lineno"] = i + continue + + if self.patch["lineno"] == 0: + self.patch["lineno"] = self.source_lineno + + if self.patch["max_num"] < self.patch["threshold"]: + self.patch["max_num"] = self.patch["threshold"] + else: + self.patch["max_num"] += 1 + + def update(self, log_title, log_content, patches): + """ + Update items in spec file + """ + self.release["num"] += 1 + self._lines[self.release["lineno"] + ] = re.sub(r"[\d]+", str(self.release["num"]), self._lines[self.release["lineno"]]) + + log_title = "* " + log_title + " " + self.version + "-" + str(self.release["num"]) + log_content = "- " + log_content + self._lines.insert(self.changelog_lineno + 1, log_title + "\n" + log_content + "\n") + + patch_list = [] + for patch in patches: + patch_list.append("Patch" + str(self.patch["max_num"]) + ": " + patch) + self.patch["max_num"] += 1 + self._lines.insert(self.patch["lineno"] + 1, "\n".join(patch_list)) + + return self.__str__() + + def __str__(self): + return "\n".join(self._lines) + + +if __name__ == "__main__": + SPEC_CONTENT = """Name: diffutils +Version: 3.7 +Release: 3 + +Source: ftp://ftp.gnu.org/gnu/diffutils/diffutils-%{version}.tar.xz + +Patch: diffutils-cmp-s-empty.patch + +%changelog +* Mon Nov 11 2019 shenyangyang 3.7-3 +- DESC:delete unneeded comments + +* Thu Oct 24 2019 shenyangyang 3.7-2 +- Type:enhancement +""" + + s = Spec(SPEC_CONTENT) + s.update("Mon Nov 11 2019 patch-tracking", "DESC:add patch files", [ + "xxx.patch", + "yyy.patch", + ]) + + print(s) + + SPEC_CONTENT = """""" + + s = Spec(SPEC_CONTENT) + s.update("Mon Nov 11 2019 patch-tracking", "DESC:add patch files", [ + "xxx.patch", + "yyy.patch", + ]) + + print(s) diff --git a/patch-tracking/setup.py b/patch-tracking/setup.py new file mode 100644 index 00000000..ca5775fa --- /dev/null +++ b/patch-tracking/setup.py @@ -0,0 +1,27 @@ +""" +setup about building of pactch tracking +""" +from setuptools import setup, find_packages + +setup( + name='patch-tracking', + version='1.0.0', + packages=find_packages(), + url='https://openeuler.org/zh/', + license='Mulan PSL v2', + author='ChenYanpan', + author_email='chenyanpan@huawei.com', + description='This is a tool for automatically tracking upstream repository code patches', + requires=['requests', 'flask', 'flask_restx', 'Flask_SQLAlchemy', 'Flask_APScheduler'], + data_files=[ + ('/etc/patch-tracking/', ['patch_tracking/settings.conf']), + ('/etc/patch-tracking/', ['patch_tracking/logging.conf']), + ('/var/patch-tracking/', ['patch_tracking/db.sqlite']), + ('/usr/bin/', ['patch_tracking/cli/patch-tracking-cli']), + ('/usr/bin/', ['patch_tracking/patch-tracking']), + ('/usr/bin/', ['patch_tracking/cli/generate_password']), + ('/etc/patch-tracking/', ['patch_tracking/self-signed.crt']), + ('/etc/patch-tracking/', ['patch_tracking/self-signed.key']), + ('/usr/lib/systemd/system/', ['patch_tracking/patch-tracking.service']), + ], +) -- Gitee From 07fa1f4d5e6b1354b2f44758fc91f9a2fcd102e6 Mon Sep 17 00:00:00 2001 From: custa <> Date: Wed, 22 Jul 2020 09:22:47 +0800 Subject: [PATCH 2/6] 'import xxx' instead of 'from xxx import yyy' --- patch-tracking/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/patch-tracking/setup.py b/patch-tracking/setup.py index ca5775fa..988d6812 100644 --- a/patch-tracking/setup.py +++ b/patch-tracking/setup.py @@ -1,12 +1,12 @@ """ setup about building of pactch tracking """ -from setuptools import setup, find_packages +import setuptools -setup( +setuptools.setup( name='patch-tracking', version='1.0.0', - packages=find_packages(), + packages=setuptools.find_packages(), url='https://openeuler.org/zh/', license='Mulan PSL v2', author='ChenYanpan', -- Gitee From 6542c78a04295cd23c3c5d23de74fad118fa4eed Mon Sep 17 00:00:00 2001 From: custa <> Date: Wed, 22 Jul 2020 09:40:55 +0800 Subject: [PATCH 3/6] fixed install setup --- patch-tracking/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/patch-tracking/README.md b/patch-tracking/README.md index 3b360b4a..77865cc8 100644 --- a/patch-tracking/README.md +++ b/patch-tracking/README.md @@ -72,8 +72,10 @@ pip3 install -I uwsgi ### 3.2 安装 +这里以 `patch-tracking-1.0.0-1.oe1.noarch.rpm` 为例 + ```shell script -rpm -ivh patch-tracking-xxx.rpm +rpm -ivh patch-tracking-1.0.0-1.oe1.noarch.rpm ``` ### 3.3 配置 -- Gitee From bae5dfe6e8c549ec3a1630aaa42eaffb16513406 Mon Sep 17 00:00:00 2001 From: custa <> Date: Wed, 22 Jul 2020 10:21:07 +0800 Subject: [PATCH 4/6] fixed 'Catching too general exception Exception' --- patch-tracking/Pipfile | 1 + patch-tracking/Pipfile.lock | 143 +++++++----------- patch-tracking/patch_tracking/api/tracking.py | 9 +- 3 files changed, 64 insertions(+), 89 deletions(-) diff --git a/patch-tracking/Pipfile b/patch-tracking/Pipfile index 13c8419e..614252ad 100644 --- a/patch-tracking/Pipfile +++ b/patch-tracking/Pipfile @@ -15,6 +15,7 @@ flask-apscheduler = "*" requests = "*" werkzeug = "*" flask-httpauth = "*" +sqlalchemy = "*" [requires] python_version = "3.7" diff --git a/patch-tracking/Pipfile.lock b/patch-tracking/Pipfile.lock index 9e63ae94..9a11622a 100644 --- a/patch-tracking/Pipfile.lock +++ b/patch-tracking/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a7833948fd05f098923413c1dadff35d6e08fad526d0ccb93a4b60f73b9f9f24" + "sha256": "d1f395f5adb9429b23b6eecefc56c0c65c91d42c7668e2b89f8280a2ce6cbbcd" }, "pipfile-spec": 6, "requires": { @@ -70,11 +70,11 @@ }, "flask-sqlalchemy": { "hashes": [ - "sha256:0b656fbf87c5f24109d859bafa791d29751fabbda2302b606881ae5485b557a5", - "sha256:fcfe6df52cd2ed8a63008ca36b86a51fa7a4b70cef1c39e5625f722fca32308e" + "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", + "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" ], "index": "pypi", - "version": "==2.4.3" + "version": "==2.4.4" }, "idna": { "hashes": [ @@ -201,7 +201,7 @@ "sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274", "sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "index": "pypi", "version": "==1.3.18" }, "tzlocal": { @@ -213,11 +213,11 @@ }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.9" + "version": "==1.25.10" }, "werkzeug": { "hashes": [ @@ -239,61 +239,61 @@ }, "cffi": { "hashes": [ - "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff", - "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b", - "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac", - "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0", - "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384", - "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26", - "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6", - "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b", - "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e", - "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd", - "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2", - "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66", - "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc", - "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8", - "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55", - "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4", - "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5", - "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d", - "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78", - "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa", - "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793", - "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f", - "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a", - "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f", - "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30", - "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f", - "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3", - "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c" - ], - "version": "==1.14.0" + "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc", + "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9", + "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792", + "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2", + "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022", + "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8", + "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96", + "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2", + "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995", + "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1", + "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849", + "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c", + "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe", + "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3", + "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90", + "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f", + "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1", + "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf", + "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa", + "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc", + "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939", + "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e", + "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0", + "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9", + "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168", + "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33", + "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f", + "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948" + ], + "version": "==1.14.1" }, "cryptography": { "hashes": [ - "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", - "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", - "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", - "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", - "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", - "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", - "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", - "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", - "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", - "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", - "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", - "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", - "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", - "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", - "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", - "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", - "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", - "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", - "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" + "sha256:0c608ff4d4adad9e39b5057de43657515c7da1ccb1807c3a27d4cf31fc923b4b", + "sha256:0cbfed8ea74631fe4de00630f4bb592dad564d57f73150d6f6796a24e76c76cd", + "sha256:124af7255ffc8e964d9ff26971b3a6153e1a8a220b9a685dc407976ecb27a06a", + "sha256:384d7c681b1ab904fff3400a6909261cae1d0939cc483a68bdedab282fb89a07", + "sha256:45741f5499150593178fc98d2c1a9c6722df88b99c821ad6ae298eff0ba1ae71", + "sha256:4b9303507254ccb1181d1803a2080a798910ba89b1a3c9f53639885c90f7a756", + "sha256:4d355f2aee4a29063c10164b032d9fa8a82e2c30768737a2fd56d256146ad559", + "sha256:51e40123083d2f946794f9fe4adeeee2922b581fa3602128ce85ff813d85b81f", + "sha256:8713ddb888119b0d2a1462357d5946b8911be01ddbf31451e1d07eaa5077a261", + "sha256:8e924dbc025206e97756e8903039662aa58aa9ba357d8e1d8fc29e3092322053", + "sha256:8ecef21ac982aa78309bb6f092d1677812927e8b5ef204a10c326fc29f1367e2", + "sha256:8ecf9400d0893836ff41b6f977a33972145a855b6efeb605b49ee273c5e6469f", + "sha256:9367d00e14dee8d02134c6c9524bb4bd39d4c162456343d07191e2a0b5ec8b3b", + "sha256:a09fd9c1cca9a46b6ad4bea0a1f86ab1de3c0c932364dbcf9a6c2a5eeb44fa77", + "sha256:ab49edd5bea8d8b39a44b3db618e4783ef84c19c8b47286bf05dfdb3efb01c83", + "sha256:bea0b0468f89cdea625bb3f692cd7a4222d80a6bdafd6fb923963f2b9da0e15f", + "sha256:bec7568c6970b865f2bcebbe84d547c52bb2abadf74cefce396ba07571109c67", + "sha256:ce82cc06588e5cbc2a7df3c8a9c778f2cb722f56835a23a68b5a7264726bb00c", + "sha256:dea0ba7fe6f9461d244679efa968d215ea1f989b9c1957d7f10c21e5c7c09ad6" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.9.2" + "version": "==3.0" }, "isort": { "hashes": [ @@ -376,33 +376,6 @@ ], "version": "==0.10.1" }, - "typed-ast": { - "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" - ], - "markers": "python_version < '3.8' and implementation_name == 'cpython'", - "version": "==1.4.1" - }, "wrapt": { "hashes": [ "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" diff --git a/patch-tracking/patch_tracking/api/tracking.py b/patch-tracking/patch_tracking/api/tracking.py index 1318abd2..4fc68bb2 100644 --- a/patch-tracking/patch_tracking/api/tracking.py +++ b/patch-tracking/patch_tracking/api/tracking.py @@ -3,6 +3,7 @@ module of issue API """ import logging from flask import request, Blueprint +from sqlalchemy.exc import SQLAlchemyError from patch_tracking.database.models import Tracking from patch_tracking.api.business import create_tracking, update_tracking from patch_tracking.api.constant import ResponseCode @@ -72,12 +73,12 @@ def post(): try: update_tracking(data) logger.info('Update tracking. Data: %s.', data) - except Exception as exception: - return ResponseCode.gen_dict(code=ResponseCode.INSERT_DATA_ERROR, data=exception) + except SQLAlchemyError as err: + return ResponseCode.gen_dict(code=ResponseCode.INSERT_DATA_ERROR, data=err) else: try: create_tracking(data) logger.info('Create tracking. Data: %s.', data) - except Exception as exception: - return ResponseCode.gen_dict(code=ResponseCode.INSERT_DATA_ERROR, data=exception) + except SQLAlchemyError as err: + return ResponseCode.gen_dict(code=ResponseCode.INSERT_DATA_ERROR, data=err) return ResponseCode.gen_dict(code=ResponseCode.SUCCESS, data=request.json) -- Gitee From 3411191da0b537491668cd22c90523c70af97940 Mon Sep 17 00:00:00 2001 From: custa <> Date: Fri, 31 Jul 2020 16:46:04 +0800 Subject: [PATCH 5/6] fixed PY033 --- patch-tracking/patch_tracking/__init__.py | 1 + patch-tracking/patch_tracking/api/__init__.py | 1 + patch-tracking/patch_tracking/cli/__init__.py | 1 + patch-tracking/patch_tracking/util/__init__.py | 1 + 4 files changed, 4 insertions(+) diff --git a/patch-tracking/patch_tracking/__init__.py b/patch-tracking/patch_tracking/__init__.py index e69de29b..10aa07df 100644 --- a/patch-tracking/patch_tracking/__init__.py +++ b/patch-tracking/patch_tracking/__init__.py @@ -0,0 +1 @@ +""" module of patch_tracking """ diff --git a/patch-tracking/patch_tracking/api/__init__.py b/patch-tracking/patch_tracking/api/__init__.py index e69de29b..45275501 100644 --- a/patch-tracking/patch_tracking/api/__init__.py +++ b/patch-tracking/patch_tracking/api/__init__.py @@ -0,0 +1 @@ +""" module of api """ diff --git a/patch-tracking/patch_tracking/cli/__init__.py b/patch-tracking/patch_tracking/cli/__init__.py index e69de29b..872a5094 100644 --- a/patch-tracking/patch_tracking/cli/__init__.py +++ b/patch-tracking/patch_tracking/cli/__init__.py @@ -0,0 +1 @@ +""" module of cli """ diff --git a/patch-tracking/patch_tracking/util/__init__.py b/patch-tracking/patch_tracking/util/__init__.py index e69de29b..34a27a79 100644 --- a/patch-tracking/patch_tracking/util/__init__.py +++ b/patch-tracking/patch_tracking/util/__init__.py @@ -0,0 +1 @@ +""" module of util """ -- Gitee From 4088555653a1e1355e88df1a3a3a273190788d0c Mon Sep 17 00:00:00 2001 From: custa <> Date: Fri, 31 Jul 2020 17:41:19 +0800 Subject: [PATCH 6/6] delete certificate --- patch-tracking/README.md | 15 +++++- patch-tracking/patch-tracking.spec | 2 - patch-tracking/patch_tracking/self-signed.crt | 30 ----------- patch-tracking/patch_tracking/self-signed.key | 52 ------------------- patch-tracking/setup.py | 2 - 5 files changed, 13 insertions(+), 88 deletions(-) delete mode 100644 patch-tracking/patch_tracking/self-signed.crt delete mode 100644 patch-tracking/patch_tracking/self-signed.key diff --git a/patch-tracking/README.md b/patch-tracking/README.md index 77865cc8..966e6bae 100644 --- a/patch-tracking/README.md +++ b/patch-tracking/README.md @@ -78,7 +78,18 @@ pip3 install -I uwsgi rpm -ivh patch-tracking-1.0.0-1.oe1.noarch.rpm ``` -### 3.3 配置 + +### 3.3 生成证书 + +```shell script +openssl req -x509 -days 3650 -subj "/CN=self-signed" \ +-nodes -newkey rsa:4096 -keyout self-signed.key -out self-signed.crt +``` + +将 `self-signed.key` 和 `self-signed.crt` 拷贝到 __/etc/patch-tracking__ 目录 + + +### 3.4 配置 在配置文件中进行对应参数的配置。 @@ -135,7 +146,7 @@ PASSWORD = "" 将`pbkdf2:sha256:150000$w38eLeRm$ebb5069ba3b4dda39a698bd1d9d7f5f848af3bd93b11e0cde2b28e9e34bfbbae`配置到`PASSWORD = ""`引号中。 -### 3.4 启动补丁跟踪服务 +### 3.5 启动补丁跟踪服务 可以使用以下两种方式启动服务: diff --git a/patch-tracking/patch-tracking.spec b/patch-tracking/patch-tracking.spec index f60fd19b..d2683e1f 100644 --- a/patch-tracking/patch-tracking.spec +++ b/patch-tracking/patch-tracking.spec @@ -52,7 +52,5 @@ rm -rf $RPM_BUILD_ROOT /usr/bin/patch-tracking /usr/bin/patch-tracking-cli /var/patch-tracking/db.sqlite -/etc/patch-tracking/self-signed.crt -/etc/patch-tracking/self-signed.key /usr/bin/generate_password /usr/lib/systemd/system/patch-tracking.service diff --git a/patch-tracking/patch_tracking/self-signed.crt b/patch-tracking/patch_tracking/self-signed.crt deleted file mode 100644 index 0b78be3e..00000000 --- a/patch-tracking/patch_tracking/self-signed.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFDTCCAvWgAwIBAgIUUYmYR5HWybac4V6yIDD4I9fiKCwwDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAwwLc2VsZi1zaWduZWQwHhcNMjAwNzA0MDM0MDQzWhcNMzAw -NzAyMDM0MDQzWjAWMRQwEgYDVQQDDAtzZWxmLXNpZ25lZDCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBALN1yRKuGsXiYL40CNnbPuGMZrcSJvH2T14TuvlK -6GyFd6KQcBMgDTcwzferw/dQS5IeGD+jpfP2qNGeH7jrti9BZj12vZWSAb4Cx/Re -5RbK3B6M7s45MCmWfMjs1J8hc42mZKr8VZ+x0xUAzQbyLd+MIBS/T7nigqaAzHBg -U9P3mB+cUDYb0YbOCP8uXif/TjRtlCYpDrX37EGOgBZFt6SFaiAOzW/JLm9szV9+ -S7zCn/lWaZb4rMd9ieoKAseCZqDz09J6sq8ncws4g/g+k3WezzUd/PlrWf+Bo+HK -q2q7rsnCnfQa51JNji8wrsM34Mm/giVtx1MpKCOr2mckbP03ouqonqb7CwqRBbsl -KIMwuYBfzZ0saurPI4AYvanTxzZDQg+PGWUIbYPGq6PFwxPYFJzRteuSempXWpny -pCNPNYow/BgZKUoiZHPRYY4vh2GfDOJQrV/islgiIg27AuCKHzSSfU1F/wNT18zh -aIEJTmRAnFIe5THqlFLe3Q4HMJ7om21KA/SuERB7VWKod2lxJ2UGb/Peg3od2AjS -w6dU4iYGtXL2fbsrtrphMK9cg964LkJOevCr0bjZXPkUst9tvBcqwDVhUJodiwqQ -jULsios7DHnZK4IteZHcaqzh7PFUpSZQFKRR6mxKSd7G52ta1+QCXNTE/sUZA1Kf -FfcNAgMBAAGjUzBRMB0GA1UdDgQWBBSeDa5DTb3b9EPGHke3Aw08o3I4LjAfBgNV -HSMEGDAWgBSeDa5DTb3b9EPGHke3Aw08o3I4LjAPBgNVHRMBAf8EBTADAQH/MA0G -CSqGSIb3DQEBCwUAA4ICAQCs0/SZEa1schHjvldJq3gd7MsZHBMAPZkbvVO7NcjF -uZ8ZnNYHQFhQNA1h40EzOnyfA2Xb0jFJE2TEFzjYVjRi7VUDM/EIh5i+ebmfS92b -mGQsGmL0AKCszwpQriuHpc9KiCQViUSnO2gWAO5TcfHbXzKkXQL6Yqk6QA3kd4lO -2v8gEyaAG/Og/rafqcOciyNqcmLCtfewfn6lxy+sEducPj5vbStqFq3is/PtDRoV -Mzef3xFt+ndGhSsegqVCAa4eLgdqGum0NA5zOqzjb+5MLVRAnF5XPITV/kPoXHWp -iQOLxtjm+bGPewEhEZMu1fOSjSHNosIFw8RBOaoPfamBI+LGCda1RZgxnOg3L1rZ -zV4DEzok9d8a7appqblI1WbhTBeTjema/82HAZxoR2W0EAG4cyVlw1um02Jw0Kqp -i9NuLscWNzWRnWpWTATlHMqA9q/Xh8F8eLKOsf3WHiY1PD2nKLZddIzqVUiLMQJV -tYB697J1tdetggt+IHHkb1xoqHj3RAwyrTODkgw5eHutOeFbiJNoGbMblhcN+z9y -EINRiPnbLYbB8FPfba9wQSHoqUORzhhOM50sUrUJx/QukqSYQ86p2tsT5tQ6Ic1i -yrhezqdRmOW2aX+2P23Y+yzBvkP5PysWcyiHjzRUHNyC9MNC7XRUIEQ/Fo+QNODb -oQ== ------END CERTIFICATE----- diff --git a/patch-tracking/patch_tracking/self-signed.key b/patch-tracking/patch_tracking/self-signed.key deleted file mode 100644 index e4d486db..00000000 --- a/patch-tracking/patch_tracking/self-signed.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -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 -HWHZfW1YKIWIprRdHko8qpGgYcCzSQ== ------END PRIVATE KEY----- diff --git a/patch-tracking/setup.py b/patch-tracking/setup.py index 988d6812..3425bc1a 100644 --- a/patch-tracking/setup.py +++ b/patch-tracking/setup.py @@ -20,8 +20,6 @@ setuptools.setup( ('/usr/bin/', ['patch_tracking/cli/patch-tracking-cli']), ('/usr/bin/', ['patch_tracking/patch-tracking']), ('/usr/bin/', ['patch_tracking/cli/generate_password']), - ('/etc/patch-tracking/', ['patch_tracking/self-signed.crt']), - ('/etc/patch-tracking/', ['patch_tracking/self-signed.key']), ('/usr/lib/systemd/system/', ['patch_tracking/patch-tracking.service']), ], ) -- Gitee