Repository: storm
Updated Branches:
  refs/heads/1.x-branch e67c55a34 -> 866517c15


STORM-2665: Adapt Kafka's release note generation script for Storm


Project: http://git-wip-us.apache.org/repos/asf/storm/repo
Commit: http://git-wip-us.apache.org/repos/asf/storm/commit/866517c1
Tree: http://git-wip-us.apache.org/repos/asf/storm/tree/866517c1
Diff: http://git-wip-us.apache.org/repos/asf/storm/diff/866517c1

Branch: refs/heads/1.x-branch
Commit: 866517c15857f6161c5da2e6e7b6445ca92da722
Parents: e67c55a
Author: Stig Rohde Døssing <s...@apache.org>
Authored: Mon Jul 31 21:48:22 2017 +0200
Committer: Stig Rohde Døssing <s...@apache.org>
Committed: Thu Aug 3 18:44:39 2017 +0200

----------------------------------------------------------------------
 dev-tools/jira-github-join.py     |   2 +-
 dev-tools/jira/__init__.py        | 285 ---------------------------------
 dev-tools/jira_github/__init__.py | 285 +++++++++++++++++++++++++++++++++
 dev-tools/release_notes.py        | 118 ++++++++++++++
 dev-tools/report/report.py        |   2 +-
 5 files changed, 405 insertions(+), 287 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/storm/blob/866517c1/dev-tools/jira-github-join.py
----------------------------------------------------------------------
diff --git a/dev-tools/jira-github-join.py b/dev-tools/jira-github-join.py
index fe0daf3..1931f19 100755
--- a/dev-tools/jira-github-join.py
+++ b/dev-tools/jira-github-join.py
@@ -15,7 +15,7 @@
 from optparse import OptionParser
 from datetime import datetime
 from github import GitHub
-from jira import JiraRepo
+from jira_github import JiraRepo
 from report.report_builder import CompleteReportBuilder
 
 

http://git-wip-us.apache.org/repos/asf/storm/blob/866517c1/dev-tools/jira/__init__.py
----------------------------------------------------------------------
diff --git a/dev-tools/jira/__init__.py b/dev-tools/jira/__init__.py
deleted file mode 100755
index c98ae31..0000000
--- a/dev-tools/jira/__init__.py
+++ /dev/null
@@ -1,285 +0,0 @@
-# -*- coding: utf-8 -*-
-#  Licensed under the Apache License, Version 2.0 (the "License");
-#  you may not use this file except in compliance with the License.
-#  You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-#  limitations under the License.
-
-import re
-import urllib
-import urllib2
-from datetime import datetime
-
-try:
-    import json
-except ImportError:
-    import simplejson as json
-
-
-def mstr(obj):
-    if obj is None:
-        return ""
-    return unicode(obj)
-
-
-def jiratime(obj):
-    if obj is None:
-        return None
-    return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S")
-
-# Regex pattern definitions
-github_user = re.compile("Git[Hh]ub user ([\w-]+)")
-github_pull = re.compile("https://github.com/[^/\s]+/[^/\s]+/pull/[0-9]+";)
-has_vote = re.compile("\s+([-+][01])\s*")
-is_diff = re.compile("--- End diff --")
-
-
-def search_group(reg, txt, group):
-    m = reg.search(txt)
-    if m is None:
-        return None
-    return m.group(group)
-
-
-class JiraComment:
-    """A comment on a JIRA"""
-
-    def __init__(self, data):
-        self.data = data
-        self.author = mstr(self.data['author']['name'])
-        self.github_author = None
-        self.githubPull = None
-        self.githubComment = (self.author == "githubbot")
-        body = self.get_body()
-        if is_diff.search(body) is not None:
-            self.vote = None
-        else:
-            self.vote = search_group(has_vote, body, 1)
-
-        if self.githubComment:
-            self.github_author = search_group(github_user, body, 1)
-            self.githubPull = search_group(github_pull, body, 0)
-
-    def get_author(self):
-        if self.github_author is not None:
-            return self.github_author
-        return self.author
-
-    def get_body(self):
-        return mstr(self.data['body'])
-
-    def get_pull(self):
-        return self.githubPull
-
-    def has_github_pull(self):
-        return self.githubPull is not None
-
-    def raw(self):
-        return self.data
-
-    def has_vote(self):
-        return self.vote is not None
-
-    def get_vote(self):
-        return self.vote
-
-    def get_created(self):
-        return jiratime(self.data['created'])
-
-
-class Jira:
-    """A single JIRA"""
-
-    def __init__(self, data, parent):
-        self.key = data['key']
-        self.fields = data['fields']
-        self.parent = parent
-        self.notes = None
-        self.comments = None
-
-    def get_id(self):
-        return mstr(self.key)
-
-    def get_description(self):
-        return mstr(self.fields['description'])
-
-    def getReleaseNote(self):
-        if self.notes is None:
-            field = self.parent.fieldIdMap['Release Note']
-            if self.fields.has_key(field):
-                self.notes = mstr(self.fields[field])
-            else:
-                self.notes = self.get_description()
-        return self.notes
-
-    def get_status(self):
-        ret = ""
-        status = self.fields['status']
-        if status is not None:
-            ret = status['name']
-        return mstr(ret)
-
-    def get_priority(self):
-        ret = ""
-        pri = self.fields['priority']
-        if pri is not None:
-            ret = pri['name']
-        return mstr(ret)
-
-    def get_assignee_email(self):
-        ret = ""
-        mid = self.fields['assignee']
-        if mid is not None:
-            ret = mid['emailAddress']
-        return mstr(ret)
-
-    def get_assignee(self):
-        ret = ""
-        mid = self.fields['assignee']
-        if mid is not None:
-            ret = mid['displayName']
-        return mstr(ret)
-
-    def get_components(self):
-        return " , ".join([comp['name'] for comp in self.fields['components']])
-
-    def get_summary(self):
-        return self.fields['summary']
-
-    def get_trimmed_summary(self):
-        limit = 40
-        summary = self.fields['summary']
-        return summary if len(summary) < limit else summary[0:limit] + "..."
-
-    def get_type(self):
-        ret = ""
-        mid = self.fields['issuetype']
-        if mid is not None:
-            ret = mid['name']
-        return mstr(ret)
-
-    def get_reporter(self):
-        ret = ""
-        mid = self.fields['reporter']
-        if mid is not None:
-            ret = mid['displayName']
-        return mstr(ret)
-
-    def get_project(self):
-        ret = ""
-        mid = self.fields['project']
-        if mid is not None:
-            ret = mid['key']
-        return mstr(ret)
-
-    def get_created(self):
-        return jiratime(self.fields['created'])
-
-    def get_updated(self):
-        return jiratime(self.fields['updated'])
-
-    def get_comments(self):
-        if self.comments is None:
-            jiraId = self.get_id()
-            comments = []
-            at = 0
-            end = 1
-            count = 100
-            while (at < end):
-                params = urllib.urlencode({'startAt': at, 'maxResults': count})
-                resp = urllib2.urlopen(self.parent.baseUrl + "/issue/" + 
jiraId + "/comment?" + params)
-                data = json.loads(resp.read())
-                if (data.has_key('errorMessages')):
-                    raise Exception(data['errorMessages'])
-                at = data['startAt'] + data['maxResults']
-                end = data['total']
-                for item in data['comments']:
-                    j = JiraComment(item)
-                    comments.append(j)
-            self.comments = comments
-        return self.comments
-
-    def has_voted_comment(self):
-        for comment in self.get_comments():
-            if comment.has_vote():
-                return True
-        return False
-
-    def get_trimmed_comments(self, limit=40):
-        comments = self.get_comments()
-        return comments if len(comments) < limit else comments[0:limit] + "..."
-
-    def raw(self):
-        return self.fields
-
-    def storm_jira_cmp(x, y):
-        xn = x.get_id().split("-")[1]
-        yn = y.get_id().split("-")[1]
-        return int(xn) - int(yn)
-
-
-class JiraRepo:
-    """A Repository for JIRAs"""
-
-    def __init__(self, baseUrl):
-        self.baseUrl = baseUrl
-        resp = urllib2.urlopen(baseUrl + "/field")
-        data = json.loads(resp.read())
-
-        self.fieldIdMap = {}
-        for part in data:
-            self.fieldIdMap[part['name']] = part['id']
-
-    def get(self, id):
-        resp = urllib2.urlopen(self.baseUrl + "/issue/" + id)
-        data = json.loads(resp.read())
-        if (data.has_key('errorMessages')):
-            raise Exception(data['errorMessages'])
-        j = Jira(data, self)
-        return j
-
-    def query(self, query):
-        jiras = {}
-        at = 0
-        end = 1
-        count = 100
-        while (at < end):
-            params = urllib.urlencode({'jql': query, 'startAt': at, 
'maxResults': count})
-            # print params
-            resp = urllib2.urlopen(self.baseUrl + "/search?%s" % params)
-            data = json.loads(resp.read())
-            if (data.has_key('errorMessages')):
-                raise Exception(data['errorMessages'])
-            at = data['startAt'] + data['maxResults']
-            end = data['total']
-            for item in data['issues']:
-                j = Jira(item, self)
-                jiras[j.get_id()] = j
-        return jiras
-
-    def unresolved_jiras(self, project):
-        """
-        :param project: The JIRA project to search for unresolved issues
-        :return: All JIRA issues that have the field resolution = Unresolved
-        """
-        return self.query("project = " + project + " AND resolution = 
Unresolved")
-
-    def open_jiras(self, project):
-        """
-        :param project: The JIRA project to search for open issues
-        :return: All JIRA issues that have the field status = Open
-        """
-        return self.query("project = " + project + " AND status = Open")
-
-    def in_progress_jiras(self, project):
-        """
-        :param project: The JIRA project to search for In Progress issues
-        :return: All JIRA issues that have the field status = 'In Progress'
-        """
-        return self.query("project = " + project + " AND status = 'In 
Progress'")

http://git-wip-us.apache.org/repos/asf/storm/blob/866517c1/dev-tools/jira_github/__init__.py
----------------------------------------------------------------------
diff --git a/dev-tools/jira_github/__init__.py 
b/dev-tools/jira_github/__init__.py
new file mode 100644
index 0000000..c98ae31
--- /dev/null
+++ b/dev-tools/jira_github/__init__.py
@@ -0,0 +1,285 @@
+# -*- coding: utf-8 -*-
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import re
+import urllib
+import urllib2
+from datetime import datetime
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+
+def mstr(obj):
+    if obj is None:
+        return ""
+    return unicode(obj)
+
+
+def jiratime(obj):
+    if obj is None:
+        return None
+    return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S")
+
+# Regex pattern definitions
+github_user = re.compile("Git[Hh]ub user ([\w-]+)")
+github_pull = re.compile("https://github.com/[^/\s]+/[^/\s]+/pull/[0-9]+";)
+has_vote = re.compile("\s+([-+][01])\s*")
+is_diff = re.compile("--- End diff --")
+
+
+def search_group(reg, txt, group):
+    m = reg.search(txt)
+    if m is None:
+        return None
+    return m.group(group)
+
+
+class JiraComment:
+    """A comment on a JIRA"""
+
+    def __init__(self, data):
+        self.data = data
+        self.author = mstr(self.data['author']['name'])
+        self.github_author = None
+        self.githubPull = None
+        self.githubComment = (self.author == "githubbot")
+        body = self.get_body()
+        if is_diff.search(body) is not None:
+            self.vote = None
+        else:
+            self.vote = search_group(has_vote, body, 1)
+
+        if self.githubComment:
+            self.github_author = search_group(github_user, body, 1)
+            self.githubPull = search_group(github_pull, body, 0)
+
+    def get_author(self):
+        if self.github_author is not None:
+            return self.github_author
+        return self.author
+
+    def get_body(self):
+        return mstr(self.data['body'])
+
+    def get_pull(self):
+        return self.githubPull
+
+    def has_github_pull(self):
+        return self.githubPull is not None
+
+    def raw(self):
+        return self.data
+
+    def has_vote(self):
+        return self.vote is not None
+
+    def get_vote(self):
+        return self.vote
+
+    def get_created(self):
+        return jiratime(self.data['created'])
+
+
+class Jira:
+    """A single JIRA"""
+
+    def __init__(self, data, parent):
+        self.key = data['key']
+        self.fields = data['fields']
+        self.parent = parent
+        self.notes = None
+        self.comments = None
+
+    def get_id(self):
+        return mstr(self.key)
+
+    def get_description(self):
+        return mstr(self.fields['description'])
+
+    def getReleaseNote(self):
+        if self.notes is None:
+            field = self.parent.fieldIdMap['Release Note']
+            if self.fields.has_key(field):
+                self.notes = mstr(self.fields[field])
+            else:
+                self.notes = self.get_description()
+        return self.notes
+
+    def get_status(self):
+        ret = ""
+        status = self.fields['status']
+        if status is not None:
+            ret = status['name']
+        return mstr(ret)
+
+    def get_priority(self):
+        ret = ""
+        pri = self.fields['priority']
+        if pri is not None:
+            ret = pri['name']
+        return mstr(ret)
+
+    def get_assignee_email(self):
+        ret = ""
+        mid = self.fields['assignee']
+        if mid is not None:
+            ret = mid['emailAddress']
+        return mstr(ret)
+
+    def get_assignee(self):
+        ret = ""
+        mid = self.fields['assignee']
+        if mid is not None:
+            ret = mid['displayName']
+        return mstr(ret)
+
+    def get_components(self):
+        return " , ".join([comp['name'] for comp in self.fields['components']])
+
+    def get_summary(self):
+        return self.fields['summary']
+
+    def get_trimmed_summary(self):
+        limit = 40
+        summary = self.fields['summary']
+        return summary if len(summary) < limit else summary[0:limit] + "..."
+
+    def get_type(self):
+        ret = ""
+        mid = self.fields['issuetype']
+        if mid is not None:
+            ret = mid['name']
+        return mstr(ret)
+
+    def get_reporter(self):
+        ret = ""
+        mid = self.fields['reporter']
+        if mid is not None:
+            ret = mid['displayName']
+        return mstr(ret)
+
+    def get_project(self):
+        ret = ""
+        mid = self.fields['project']
+        if mid is not None:
+            ret = mid['key']
+        return mstr(ret)
+
+    def get_created(self):
+        return jiratime(self.fields['created'])
+
+    def get_updated(self):
+        return jiratime(self.fields['updated'])
+
+    def get_comments(self):
+        if self.comments is None:
+            jiraId = self.get_id()
+            comments = []
+            at = 0
+            end = 1
+            count = 100
+            while (at < end):
+                params = urllib.urlencode({'startAt': at, 'maxResults': count})
+                resp = urllib2.urlopen(self.parent.baseUrl + "/issue/" + 
jiraId + "/comment?" + params)
+                data = json.loads(resp.read())
+                if (data.has_key('errorMessages')):
+                    raise Exception(data['errorMessages'])
+                at = data['startAt'] + data['maxResults']
+                end = data['total']
+                for item in data['comments']:
+                    j = JiraComment(item)
+                    comments.append(j)
+            self.comments = comments
+        return self.comments
+
+    def has_voted_comment(self):
+        for comment in self.get_comments():
+            if comment.has_vote():
+                return True
+        return False
+
+    def get_trimmed_comments(self, limit=40):
+        comments = self.get_comments()
+        return comments if len(comments) < limit else comments[0:limit] + "..."
+
+    def raw(self):
+        return self.fields
+
+    def storm_jira_cmp(x, y):
+        xn = x.get_id().split("-")[1]
+        yn = y.get_id().split("-")[1]
+        return int(xn) - int(yn)
+
+
+class JiraRepo:
+    """A Repository for JIRAs"""
+
+    def __init__(self, baseUrl):
+        self.baseUrl = baseUrl
+        resp = urllib2.urlopen(baseUrl + "/field")
+        data = json.loads(resp.read())
+
+        self.fieldIdMap = {}
+        for part in data:
+            self.fieldIdMap[part['name']] = part['id']
+
+    def get(self, id):
+        resp = urllib2.urlopen(self.baseUrl + "/issue/" + id)
+        data = json.loads(resp.read())
+        if (data.has_key('errorMessages')):
+            raise Exception(data['errorMessages'])
+        j = Jira(data, self)
+        return j
+
+    def query(self, query):
+        jiras = {}
+        at = 0
+        end = 1
+        count = 100
+        while (at < end):
+            params = urllib.urlencode({'jql': query, 'startAt': at, 
'maxResults': count})
+            # print params
+            resp = urllib2.urlopen(self.baseUrl + "/search?%s" % params)
+            data = json.loads(resp.read())
+            if (data.has_key('errorMessages')):
+                raise Exception(data['errorMessages'])
+            at = data['startAt'] + data['maxResults']
+            end = data['total']
+            for item in data['issues']:
+                j = Jira(item, self)
+                jiras[j.get_id()] = j
+        return jiras
+
+    def unresolved_jiras(self, project):
+        """
+        :param project: The JIRA project to search for unresolved issues
+        :return: All JIRA issues that have the field resolution = Unresolved
+        """
+        return self.query("project = " + project + " AND resolution = 
Unresolved")
+
+    def open_jiras(self, project):
+        """
+        :param project: The JIRA project to search for open issues
+        :return: All JIRA issues that have the field status = Open
+        """
+        return self.query("project = " + project + " AND status = Open")
+
+    def in_progress_jiras(self, project):
+        """
+        :param project: The JIRA project to search for In Progress issues
+        :return: All JIRA issues that have the field status = 'In Progress'
+        """
+        return self.query("project = " + project + " AND status = 'In 
Progress'")

http://git-wip-us.apache.org/repos/asf/storm/blob/866517c1/dev-tools/release_notes.py
----------------------------------------------------------------------
diff --git a/dev-tools/release_notes.py b/dev-tools/release_notes.py
new file mode 100644
index 0000000..c1e053a
--- /dev/null
+++ b/dev-tools/release_notes.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Usage: release_notes.py <version> > RELEASE_NOTES.html
+
+Depends on https://pypi.python.org/pypi/jira/, please use pip to install this 
module.
+
+Generates release notes for a Storm release by generating an HTML doc 
containing some introductory information about the
+ release with links to the Storm docs followed by a list of issues resolved in 
the release. The script will fail if it finds
+ any unresolved issues still marked with the target release. You should run 
this script after either resolving all issues or
+ moving outstanding issues to a later release.
+
+"""
+
+from jira import JIRA
+import itertools, sys
+
+if len(sys.argv) < 2:
+    print >>sys.stderr, "Usage: release_notes.py <version>"
+    sys.exit(1)
+
+version = sys.argv[1]
+
+JIRA_BASE_URL = 'https://issues.apache.org/jira'
+MAX_RESULTS = 100 # This is constrained for cloud instances so we need to fix 
this value
+
+def get_issues(jira, query, **kwargs):
+    """
+    Get all issues matching the JQL query from the JIRA instance. This handles 
expanding paginated results for you. Any additional keyword arguments are 
forwarded to the JIRA.search_issues call.
+    """
+    results = []
+    startAt = 0
+    new_results = None
+    while new_results == None or len(new_results) == MAX_RESULTS:
+        new_results = jira.search_issues(query, startAt=startAt, 
maxResults=MAX_RESULTS, **kwargs)
+        results += new_results
+        startAt += len(new_results)
+    return results
+
+def issue_link(issue):
+    return "%s/browse/%s" % (JIRA_BASE_URL, issue.key)
+
+
+if __name__ == "__main__":
+    apache = JIRA(JIRA_BASE_URL)
+    issues = get_issues(apache, 'project=STORM and fixVersion=%s' % version)
+    if not issues:
+        print >>sys.stderr, "Didn't find any issues for the target fix version"
+        sys.exit(1)
+
+    # Some resolutions, including a lack of resolution, indicate that the bug 
hasn't actually been addressed and we shouldn't even be able to create a 
release until they are fixed
+    UNRESOLVED_RESOLUTIONS = [None,
+                              "Unresolved",
+                              "Duplicate",
+                              "Invalid",
+                              "Not A Problem",
+                              "Not A Bug",
+                              "Won't Fix",
+                              "Incomplete",
+                              "Cannot Reproduce",
+                              "Later",
+                              "Works for Me",
+                              "Workaround",
+                              "Information Provided"
+                              ]
+    unresolved_issues = [issue for issue in issues if issue.fields.resolution 
in UNRESOLVED_RESOLUTIONS or issue.fields.resolution.name in 
UNRESOLVED_RESOLUTIONS]
+    if unresolved_issues:
+        print >>sys.stderr, "The release is not completed since unresolved 
issues or improperly resolved issues were found still tagged with this release 
as the fix version:"
+        for issue in unresolved_issues:
+            print >>sys.stderr, "Unresolved issue: %15s %20s %s" % (issue.key, 
issue.fields.resolution, issue_link(issue))
+        print >>sys.stderr
+        print >>sys.stderr, "Note that for some resolutions, you should simply 
remove the fix version as they have not been truly fixed in this release."
+        sys.exit(1)
+
+    # Get list of (issue type, [issues]) sorted by the issue ID type, with 
each subset of issues sorted by their key so they
+    # are in increasing order of bug #. To get a nice ordering of the issue 
types we customize the key used to sort by issue
+    # type a bit to ensure features and improvements end up first.
+    def issue_type_key(issue):
+        if issue.fields.issuetype.name == 'New Feature':
+            return -2
+        if issue.fields.issuetype.name == 'Improvement':
+            return -1
+        return issue.fields.issuetype.id
+    by_group = [(k,sorted(g, key=lambda issue: issue.id)) for k,g in 
itertools.groupby(sorted(issues, key=issue_type_key), lambda issue: 
issue.fields.issuetype.name)]
+
+    print "<!DOCTYPE html>"
+    print "<html lang=\"en\">"
+    print "<head>"
+    print "<meta charset=\"utf-8\">"
+    print "<title>Storm %(version)s Release Notes</title>" % { 'version': 
version }
+    print "</head>"
+    print "<body>"
+    print "<h1>Release Notes for Storm %s</h1>" % version
+    print """<p>JIRA issues addressed in the %(version)s release of Storm. 
Documentation for this
+    release is available at the <a href="http://storm.apache.org/";>Apache Storm
+    project site</a>.</p>""" % { 'version': version }
+    for itype, issues in by_group:
+        print "<h2>%s</h2>" % itype
+        print "<ul>"
+        for issue in issues:
+            print '<li>[<a href="%(link)s">%(key)s</a>] - %(summary)s</li>' % 
{'key': issue.key, 'link': issue_link(issue), 'summary': issue.fields.summary}
+        print "</ul>"
+    print "</body>"
+    print "</html>"

http://git-wip-us.apache.org/repos/asf/storm/blob/866517c1/dev-tools/report/report.py
----------------------------------------------------------------------
diff --git a/dev-tools/report/report.py b/dev-tools/report/report.py
index 477a498..d2df077 100644
--- a/dev-tools/report/report.py
+++ b/dev-tools/report/report.py
@@ -14,7 +14,7 @@
 
 from datetime import datetime
 from github import mstr
-from jira import Jira
+from jira_github import Jira
 from formatter import Formatter, encode
 
 

Reply via email to