From e5cf96341983888f2b899372b34ce80436b8d2d2 Mon Sep 17 00:00:00 2001
From: Anton Medvedev <amedvede@redhat.com>
Date: Jun 27 2025 07:53:12 +0000
Subject: Adding request-unretirement command


Signed-off-by: Anton Medvedev <amedvede@redhat.com>

---

diff --git a/fedpkg/cli.py b/fedpkg/cli.py
index e5ed34b..3d7adb5 100644
--- a/fedpkg/cli.py
+++ b/fedpkg/cli.py
@@ -21,6 +21,9 @@ import re
 import shutil
 import textwrap
 from datetime import datetime
+
+import arrow
+
 # Use deprecated pkg_resources if importlib isn't available (python 3.6)
 try:
     from importlib.metadata import distribution
@@ -44,9 +47,11 @@ from fedpkg.utils import (assert_new_tests_repo, assert_valid_epel_package,
                           do_fork, expand_release, get_dist_git_url,
                           get_fedora_release_state, get_pagure_branches,
                           get_release_branches, get_stream_branches, is_epel,
-                          new_pagure_issue, sl_list_to_dict, verify_sls)
+                          new_pagure_issue, sl_list_to_dict, verify_sls,
+                          get_last_commit_date)
 
 RELEASE_BRANCH_REGEX = r'^(f\d+|el\d+|eln|epel\d+|epel\d+\.\d+)$'
+BUGZILLA_URL_REGEX = r"https:\/\/bugzilla\.redhat\.com\/show_bug\.cgi\?id=(\d{7})"
 LOCAL_PACKAGE_CONFIG = 'package.cfg'
 
 BODHI_TEMPLATE = """\
@@ -137,6 +142,7 @@ class fedpkgClient(cliClient):
         self.register_set_distgit_token()
         self.register_set_pagure_token()
         self.register_do_disable_monitoring()
+        self.register_request_unretirement()
 
     def setup_completers(self):
         """
@@ -499,6 +505,79 @@ class fedpkgClient(cliClient):
         )
         request_branch_parser.set_defaults(command=self.request_branch)
 
+    def register_request_unretirement(self):
+        help_msg = "Request an unretirement of package branch"
+        description = textwrap.dedent("""
+            Request an unretirement of package branch
+
+            Branch name could be one of current active Fedora and EPEL releases. Use
+            command ``{0} releases-info`` to get release names that can be used to request
+            a branch.
+
+            Below are various examples of requesting an unretirement of package branch.
+
+            Request an unretirement of `rawhide` branch inside cloned repository.
+            It will use discovered username use `--user USER` to override it.
+            Also if branch was retired more
+            than 8 weeks ago it will ask for opened bugzilla ticket.
+
+                {0} request-unretirement
+
+            Request an unretirement of few branches:
+
+                {0} request-unretirement -b rawhide f40
+
+            Request an unretirement of branch that was retired more than 8 weeks ago
+            and requires bugzilla review to proceed:
+
+                {0} request-unretirement --bz_bug_id BUG_ID
+
+            Request an unretirement of package with different namespace than `rpms`:
+
+                {0} request-unretirement --namespace test
+
+            Request an unretirement outside package repository:
+
+                {0} request-unretirement --repo name_of_package
+        """.format(self.name))
+        request_unretirement_parser = self.subparsers.add_parser(
+            "request-unretirement",
+            formatter_class=argparse.RawTextHelpFormatter,
+            help=help_msg,
+            description=description,
+        )
+        request_unretirement_parser.add_argument(
+            '--repo',
+            required=False,
+            dest='repo_name',
+            metavar='NAME',
+            help='Repository name to unretire some branches in.'
+        )
+        request_unretirement_parser.add_argument(
+            '--namespace',
+            required=False,
+            dest='repo_ns_name',
+            metavar='NS',
+            default='rpms',
+            help='Repository namespace name, as a default use `rpm`.',
+        )
+        request_unretirement_parser.add_argument(
+            "--bz_bug_id",
+            required=False,
+            dest="bz_bug_id",
+            metavar="BUG_ID",
+            default=None,
+            help="Bugzilla bug ID with re-review."
+        )
+        request_unretirement_parser.add_argument(
+            "-b", "--branches",
+            required=False,
+            nargs='+',
+            dest="branches",
+            help="Comma-separated list of branches for unretirement."
+        )
+        request_unretirement_parser.set_defaults(command=self.request_unretirement)
+
     def register_do_fork(self):
         help_msg = 'Create a new fork of the current repository'
         distgit_section = '{0}.distgit'.format(self.name)
@@ -1334,6 +1413,140 @@ class fedpkgClient(cliClient):
                     anongiturl=anongiturl,
                 )
 
+    def request_unretirement(self):
+        if self.args.repo_name:
+            self.cmd.repo_name = self.args.repo_name
+            self.cmd.ns = self.args.repo_ns_name
+
+        try:
+            active_branch = self.cmd.repo.active_branch.name
+        except rpkgError:
+            active_branch = None
+
+        if not self.args.branches:
+            if not active_branch:
+                branches = ["rawhide"]
+            else:
+                branches = [active_branch]
+        else:
+            branches = self.args.branches
+
+        self._request_unretirement(
+            logger=self.log,
+            repo_name=self.cmd.repo_name,
+            ns=self.cmd.ns,
+            branches=branches,
+            bugzila_url=self.args.bz_bug_id,
+            fas_name=self.cmd.user,
+            name=self.name,
+            config=self.config,
+        )
+
+    @staticmethod
+    def _request_unretirement(
+            logger, repo_name, ns, branches, bugzila_url, fas_name, name, config
+    ):
+        """ Implementation of `request_unretirement`.
+
+        Submits a request for an unretretirement of package branch.
+
+        :param logger: A logger object.
+        :param repo_name: The string of the repo name.
+        :param ns: The string of pacakge namespace.
+        :param branches: The list of branches that need to be unretired.
+        :param bugzila_url: The URL of the bugzilla review.
+            Typically, the value of `self.args.bz_url`, None if not needed.
+        :param fas_name: The string of fas name of user.
+            Typically, value is `self.cmd.user`.
+        :param name: A string representing which section of the config should be
+            used.  Typically, the value of `self.name`.
+        :param config: A dict containing the configuration, loaded from file.
+            Typically, the value of `self.config`.
+        """
+        distgit_section = '{0}.distgit'.format(name)
+        distgit_url = config_get_safely(config, distgit_section, 'apibaseurl')
+        bodhi_url = config.get('{0}.bodhi'.format(name), 'url')
+        current_time = arrow.now()
+        bugzilla_need = False
+
+        release_branches = list(itertools.chain(
+            *list(get_release_branches(bodhi_url).values())))
+        release_branches.append('rawhide')
+
+        requested_branches = branches.copy()
+        for branch in requested_branches:
+            if branch not in release_branches:
+                branches.remove(branch)
+                print(
+                    f"Branch {0} can't be unretired, because it is not an release branch."
+                    .format(branch)
+                )
+
+        if not branches:
+            raise rpkgError(
+                "All requested branches {0} are not release branches."
+                " So there is nothing to unretire.".format(requested_branches)
+            )
+
+        for branch in branches:
+            commit_date = get_last_commit_date(distgit_url, ns, repo_name, branch)
+            commited_time = arrow.get(commit_date)
+            difference_in_days = (current_time - commited_time).days
+            if difference_in_days > 56:  # 56 days is 8 weeks
+                bugzilla_need = True
+                if bugzila_url is None:
+                    raise rpkgError(
+                        "Bugzilla url should be provided, "
+                        "because last commit was made more than 8 weeks ago."
+                    )
+
+        # is bug has fedora-review+ flag
+        def get_bug_id(bz_info):
+            if not bz_info:
+                return None
+            match = re.search(BUGZILLA_URL_REGEX, bz_info)
+            if match:
+                bz_bug_id = match.group(1)
+                return bz_bug_id
+            else:
+                return bz_info
+
+        bug_id = get_bug_id(bugzila_url)
+
+        if bugzilla_need:
+            bz_url = config.get('{0}.bugzilla'.format(name), 'url')
+            bz_client = BugzillaClient(bz_url)
+            # get_review_bug checks if the bug has fedora_review+ flag
+            bz_client.get_review_bug(bug_id, ns, repo_name)
+
+        # Ticket creation
+        pagure_section = '{0}.pagure'.format(name)
+        pagure_url = config_get_safely(config, pagure_section, 'url')
+        pagure_token = config_get_safely(config, pagure_section, 'token')
+
+        ticket_body = {
+            'action': 'unretirement',
+            'name': repo_name,
+            'type': ns,
+            'branches': branches,
+            'review_bugzilla': bug_id,
+            'maintainer': fas_name,
+        }
+        ticket_body = json.dumps(ticket_body, indent=True)
+        ticket_body = '```\n{0}\n```'.format(ticket_body)
+        ticket_title = 'Unretire {0}'.format(repo_name)
+
+        print(
+            new_pagure_issue(
+                logger=logger,
+                url=pagure_url,
+                token=pagure_token,
+                title=ticket_title,
+                body=ticket_body,
+                cli_name=name,
+            )
+        )
+
     def do_distgit_fork(self):
         """create fork of the distgit repository
         That includes creating fork itself using API call and then adding
diff --git a/fedpkg/utils.py b/fedpkg/utils.py
index 1ce9177..652a28e 100644
--- a/fedpkg/utils.py
+++ b/fedpkg/utils.py
@@ -13,6 +13,7 @@
 import json
 import re
 from datetime import datetime, timezone
+import tempfile
 
 import git
 import requests
@@ -653,7 +654,32 @@ def disable_monitoring(logger, base_url, token, repo_name, namespace, cli_name):
         if re.search(r"Invalid or expired token", rv_error, re.IGNORECASE):
             base_error_msg += '\nFor invalid or expired tokens please ' \
                 'set a new token in your user configuration with:' \
-                '\n\n\t{0} set-distgit-token\n'.format(cli_name)
+                '\n\n\t{0} set-distgit-token <token>\n'.format(cli_name)
         raise rpkgError(base_error_msg.format(rv_error))
 
     logger.info("Monitoring of the project was sucessfully disabled.")
+
+
+def get_last_commit_date(base_url, namespace, repo_name, branch):
+    """
+    Gets the last commit date of a repository.
+    :param base_url: a string of the URL repository
+    :param namespace: a string of the namespace
+    :param repo_name: a string of the repository name
+    :param branch: a string of the branch
+    :return: a string of the last commit date
+    """
+    git_url = "{0}/{1}/{2}.git".format(base_url, namespace, repo_name)
+    with tempfile.TemporaryDirectory() as temp_dir:
+        try:
+            git_repo = git.Repo.clone_from(git_url, temp_dir)
+
+            git_repo.git.checkout(branch)
+            last_commit = git_repo.head.commit
+            last_commit_date = last_commit.committed_date
+
+            return last_commit_date
+        except Exception:
+            error_msg = ("Unable to get last commit date. Try to check repo name and namespace "
+                         "if it exists.")
+            raise rpkgError(error_msg)
diff --git a/test/fedpkg-stage.conf b/test/fedpkg-stage.conf
index 60fde62..541ddad 100644
--- a/test/fedpkg-stage.conf
+++ b/test/fedpkg-stage.conf
@@ -19,6 +19,7 @@ url = https://bugzilla.stg.example.com
 
 [fedpkg-stage.pagure]
 url = https://pagure.stg.example.com/
+token = TOKEN
 
 [fedpkg-stage.distgit]
 apibaseurl = https://src.example.com
diff --git a/test/test_cli.py b/test/test_cli.py
index a89f044..c2f0883 100644
--- a/test/test_cli.py
+++ b/test/test_cli.py
@@ -24,6 +24,8 @@ from tempfile import mkdtemp, mkstemp
 from unittest.mock import Mock, PropertyMock, call, patch
 
 import git
+import arrow
+
 # Use deprecated pkg_resources if packaging library isn't available (python 3.6)
 try:
     from packaging.version import parse as parse_version
@@ -2526,3 +2528,274 @@ class TestSetToken(CliTestCase):
                             'rpkg',
                             '{0}.conf'.format(cli.name))
         self.assertEqual(os.stat(PATH).st_mode & 0o077, 0)
+
+
+class TestRequestUnretirement(CliTestCase):
+    """Test the request-unretirement cli command."""
+
+    def setUp(self):
+        super(TestRequestUnretirement, self).setUp()
+
+    def tearDown(self):
+        super(TestRequestUnretirement, self).tearDown()
+
+    def get_cli(self, cli_cmd, name='fedpkg-stage', cfg="fedpkg-stage.conf",
+                user_cfg="fedpkg-user.conf"):
+        with patch('sys.argv', new=cli_cmd):
+            return self.new_cli(name=name, cfg=cfg, user_cfg=user_cfg)
+
+    @patch("fedpkg.cli.get_last_commit_date")
+    @patch("fedpkg.cli.get_release_branches")
+    @patch("requests.post")
+    @patch('sys.stdout', new=io.StringIO())
+    def test_request_unretirement(self, mock_request_post, mock_grb, mock_gcd):
+        """Tests request-unretirement command."""
+        mock_grb.return_value = {
+            'fedora': ['f39', 'f40', 'f41'],
+            'epel': ['epel9', 'epel10'],
+        }
+
+        mock_rv = Mock()
+        mock_rv.ok = True
+        mock_rv.json.return_value = {'issue': {'id': 41}}
+        mock_request_post.return_value = mock_rv
+
+        commit_time = arrow.now().shift(days=-1).timestamp()
+        mock_gcd.return_value = commit_time
+
+        self.run_cmd(['git', 'checkout', '-b', 'f40'], cwd=self.cloned_repo_path)
+
+        cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'request-unretirement']
+
+        cli = self.get_cli(cli_cmd)
+        cli.request_unretirement()
+
+        expected_issue_content = {
+            'action': 'unretirement',
+            'name': 'testpkg',
+            'type': 'rpms',
+            'branches': ['f40'],
+            'review_bugzilla': None,
+            'maintainer': 'root',
+        }
+
+        post_data = mock_request_post.call_args_list[0][1]['data']
+        actual_issue_content = json.loads(json.loads(
+            post_data)['issue_content'].strip('```'))
+        self.assertEqual(expected_issue_content, actual_issue_content)
+        output = sys.stdout.getvalue().strip()
+        expected_output = ('https://pagure.stg.example.com/releng/'
+                           'fedora-scm-requests/issue/41')
+        self.assertEqual(output, expected_output)
+
+    @patch("fedpkg.cli.get_release_branches")
+    @patch("requests.post")
+    @patch('sys.stdout', new=io.StringIO())
+    def test_request_unretirement_not_release_branch(self, mock_request_post, mock_grb):
+        """Tests request-unretirement command on branch that is not a release branch."""
+        mock_grb.return_value = {
+            'fedora': ['f39', 'f40', 'f41'],
+            'epel': ['epel9', 'epel10'],
+        }
+
+        mock_rv = Mock()
+        mock_rv.ok = True
+        mock_rv.json.return_value = {'issue': {'id': 41}}
+        mock_request_post.return_value = mock_rv
+
+        expected_error = (
+            "All requested branches {0} are not release branches. So there is nothing to unretire."
+            .format(['f15']))
+
+        self.run_cmd(['git', 'checkout', '-b', 'f15'], cwd=self.cloned_repo_path)
+
+        cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'request-unretirement']
+
+        cli = self.get_cli(cli_cmd)
+        try:
+            cli.request_unretirement()
+            assert False, 'rpkgError not raised'
+        except rpkgError as error:
+            self.assertEqual(str(error), expected_error)
+
+    @patch("fedpkg.cli.get_last_commit_date")
+    @patch("fedpkg.cli.get_release_branches")
+    @patch("requests.post")
+    @patch('sys.stdout', new=io.StringIO())
+    def test_request_unretirement_bugzilla_needed_but_not_provided(
+            self, mock_request_post, mock_grb, mock_gcd
+    ):
+        """Tests request-unretirement that require to check bugzilla, but bug id wasn't provided"""
+        mock_grb.return_value = {
+            'fedora': ['f39', 'f40', 'f41'],
+            'epel': ['epel9', 'epel10'],
+        }
+
+        mock_rv = Mock()
+        mock_rv.ok = True
+        mock_rv.json.return_value = {'issue': {'id': 41}}
+        mock_request_post.return_value = mock_rv
+
+        commit_time = arrow.now().shift(days=-57).timestamp()
+        mock_gcd.return_value = commit_time
+
+        expected_error = (
+            "Bugzilla url should be provided, "
+            "because last commit was made more than 8 weeks ago."
+        )
+
+        self.run_cmd(['git', 'checkout', '-b', 'f40'], cwd=self.cloned_repo_path)
+
+        cli_cmd = ['fedpkg-stage', '--path', self.cloned_repo_path, 'request-unretirement']
+        cli = self.get_cli(cli_cmd)
+
+        try:
+            cli.request_unretirement()
+            assert False, 'rpkgError not raised'
+        except rpkgError as error:
+            self.assertEqual(str(error), expected_error)
+
+    @patch("fedpkg.bugzilla.BugzillaClient.get_review_bug")
+    @patch("fedpkg.cli.get_last_commit_date")
+    @patch("fedpkg.cli.get_release_branches")
+    @patch("requests.post")
+    @patch('sys.stdout', new=io.StringIO())
+    def test_request_unretirement_bz_id_provided(
+            self, mock_request_post, mock_grb, mock_gcd, mock_bz_grb
+    ):
+        """Tests request-unretirement command when bz_id provided"""
+        mock_grb.return_value = {
+            'fedora': ['f39', 'f40', 'f41'],
+            'epel': ['epel9', 'epel10'],
+        }
+
+        mock_rv = Mock()
+        mock_rv.ok = True
+        mock_rv.json.return_value = {'issue': {'id': 41}}
+        mock_request_post.return_value = mock_rv
+
+        commit_time = arrow.now().shift(days=-57).timestamp()
+        mock_gcd.return_value = commit_time
+
+        self.run_cmd(['git', 'checkout', '-b', 'f40'], cwd=self.cloned_repo_path)
+
+        cli_cmd = [
+            'fedpkg-stage', '--path', self.cloned_repo_path, 'request-unretirement',
+            '--bz_bug_id', '1234567']
+
+        cli = self.get_cli(cli_cmd)
+        cli.request_unretirement()
+
+        expected_issue_content = {
+            'action': 'unretirement',
+            'name': 'testpkg',
+            'type': 'rpms',
+            'branches': ['f40'],
+            'review_bugzilla': '1234567',
+            'maintainer': 'root',
+        }
+
+        post_data = mock_request_post.call_args_list[0][1]['data']
+        actual_issue_content = json.loads(json.loads(
+            post_data)['issue_content'].strip('```'))
+        self.assertEqual(expected_issue_content, actual_issue_content)
+        output = sys.stdout.getvalue().strip()
+        expected_output = ('https://pagure.stg.example.com/releng/'
+                           'fedora-scm-requests/issue/41')
+        self.assertEqual(output, expected_output)
+
+    @patch("fedpkg.bugzilla.BugzillaClient.get_review_bug")
+    @patch("fedpkg.cli.get_last_commit_date")
+    @patch("fedpkg.cli.get_release_branches")
+    @patch("requests.post")
+    @patch('sys.stdout', new=io.StringIO())
+    def test_request_unretirement_bz_url_provided(
+            self, mock_request_post, mock_grb, mock_gcd, mock_bz_grb
+    ):
+        """Tests request-unretirement command when bz url provided"""
+        mock_grb.return_value = {
+            'fedora': ['f39', 'f40', 'f41'],
+            'epel': ['epel9', 'epel10'],
+        }
+
+        mock_rv = Mock()
+        mock_rv.ok = True
+        mock_rv.json.return_value = {'issue': {'id': 41}}
+        mock_request_post.return_value = mock_rv
+
+        commit_time = arrow.now().shift(days=-57).timestamp()
+        mock_gcd.return_value = commit_time
+
+        self.run_cmd(['git', 'checkout', '-b', 'f40'], cwd=self.cloned_repo_path)
+
+        cli_cmd = [
+            'fedpkg-stage', '--path', self.cloned_repo_path, 'request-unretirement',
+            '--bz_bug_id', 'https://bugzilla.redhat.com/show_bug.cgi?id=1234567']
+
+        cli = self.get_cli(cli_cmd)
+        cli.request_unretirement()
+
+        expected_issue_content = {
+            'action': 'unretirement',
+            'name': 'testpkg',
+            'type': 'rpms',
+            'branches': ['f40'],
+            'review_bugzilla': '1234567',
+            'maintainer': 'root',
+        }
+
+        post_data = mock_request_post.call_args_list[0][1]['data']
+        actual_issue_content = json.loads(json.loads(
+            post_data)['issue_content'].strip('```'))
+        self.assertEqual(expected_issue_content, actual_issue_content)
+        output = sys.stdout.getvalue().strip()
+        expected_output = ('https://pagure.stg.example.com/releng/'
+                           'fedora-scm-requests/issue/41')
+        self.assertEqual(output, expected_output)
+
+    @patch("fedpkg.cli.get_last_commit_date")
+    @patch("fedpkg.cli.get_release_branches")
+    @patch("requests.post")
+    @patch('sys.stdout', new=io.StringIO())
+    def test_request_unretirement_repo_name_provided(
+            self, mock_request_post, mock_grb, mock_gcd
+    ):
+        """Tests request-unretirement command."""
+        mock_grb.return_value = {
+            'fedora': ['f39', 'f40', 'f41'],
+            'epel': ['epel9', 'epel10'],
+        }
+
+        mock_rv = Mock()
+        mock_rv.ok = True
+        mock_rv.json.return_value = {'issue': {'id': 41}}
+        mock_request_post.return_value = mock_rv
+
+        commit_time = arrow.now().shift(days=-1).timestamp()
+        mock_gcd.return_value = commit_time
+
+        self.run_cmd(['git', 'checkout', '-b', 'f40'], cwd=self.cloned_repo_path)
+        cli_cmd = [
+            'fedpkg-stage', '--path', self.cloned_repo_path, 'request-unretirement',
+            '--repo', 'belusha'
+        ]
+        cli = self.get_cli(cli_cmd)
+        cli.request_unretirement()
+
+        expected_issue_content = {
+            'action': 'unretirement',
+            'name': 'belusha',
+            'type': 'rpms',
+            'branches': ['f40'],
+            'review_bugzilla': None,
+            'maintainer': 'root',
+        }
+
+        post_data = mock_request_post.call_args_list[0][1]['data']
+        actual_issue_content = json.loads(json.loads(
+            post_data)['issue_content'].strip('```'))
+        self.assertEqual(expected_issue_content, actual_issue_content)
+        output = sys.stdout.getvalue().strip()
+        expected_output = ('https://pagure.stg.example.com/releng/'
+                           'fedora-scm-requests/issue/41')
+        self.assertEqual(output, expected_output)
diff --git a/test/test_utils.py b/test/test_utils.py
index 051e813..316fd44 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -14,7 +14,10 @@ from configparser import NoOptionError, NoSectionError
 import json
 import unittest
 from unittest.mock import Mock, patch
+
+import git
 from requests.exceptions import ConnectionError
+
 from fedpkg import utils
 from freezegun import freeze_time
 from pyrpkg.errors import rpkgError
@@ -644,6 +647,32 @@ class TestGetFedoraReleaseState(unittest.TestCase):
                                utils.get_fedora_release_state, config, 'fedpkg', 'F30M')
 
 
+class TestGetLastCommitDate(unittest.TestCase):
+    """Test get_last_commit_date"""
+
+    @patch("git.Repo")
+    def test_get_last_commit_date_success(self, mock_git_repo):
+        expected_date = 1712761897
+        mock_repo = mock_git_repo.clone_from.return_value
+        mock_commit = mock_repo.head.commit
+        mock_commit.committed_date = expected_date
+
+        result = utils.get_last_commit_date('base_url', 'namespace',
+                                            'repo_name', 'rawhide')
+
+        self.assertEqual(result, expected_date)
+
+    @patch("git.Repo")
+    def test_get_last_commit_date_exception(self, mock_git_repo):
+        error_msg = ("Unable to get last commit date. Try to check repo name and namespace "
+                     "if it exists.")
+        mock_git_repo.clone_from.side_effect = git.exc.GitCommandError("some error")
+
+        with self.assertRaises(rpkgError, msg=error_msg):
+            utils.get_last_commit_date(
+                "url", "ns", "name", "branch")
+
+
 class TestGetReleaseBranches(unittest.TestCase):
     """Test utils.get_release_branches"""
 
diff --git a/tests-requirements.txt b/tests-requirements.txt
index 178e74e..71785f5 100644
--- a/tests-requirements.txt
+++ b/tests-requirements.txt
@@ -3,6 +3,11 @@ coverage<5.0.0
 cccolutils
 gitpython
 freezegun
+jsonschema[format]>=2.5.1,<4.0.0
+bravado-core==5.16.1
+bravado<12,>=10.6.0
+fasjson-client
 rpm < 0.0.3
 pytest
 pytest-cov
+arrow

