From 95a34873da2e97be3ae60bb13046a73db122334f Mon Sep 17 00:00:00 2001 From: sidpremkumar Date: Jul 19 2019 18:38:50 +0000 Subject: Add ReadTheDocs documentation * Ensured that all function docstings were formatted correct * Added docs folder to hold relevent doc informations * Plan to deploy through ReadTheDocs --- diff --git a/.gitignore b/.gitignore index 65e5b0f..53c38b5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ rpmbuild-output venv/* venv3/* xunit-tests.xml +docs/build/* diff --git a/docs/docs-requirements.txt b/docs/docs-requirements.txt new file mode 100644 index 0000000..52b04f2 --- /dev/null +++ b/docs/docs-requirements.txt @@ -0,0 +1 @@ +sphinx_rtd_theme \ No newline at end of file diff --git a/docs/source/adding-new-repo-guide.rst b/docs/source/adding-new-repo-guide.rst new file mode 100644 index 0000000..4998af0 --- /dev/null +++ b/docs/source/adding-new-repo-guide.rst @@ -0,0 +1,25 @@ +Adding New Repos Guide +======================= + +Have you ever wanted to add new upstream repos? Well now you can! + +1. First ensure that your upstream repo is on the Fed Message Bus +2. Now add two new functions to `upstream.py` + * :code:`def hande_REPO-NAME_message(msg, config)` + * This function will take in a fedmessage message (msg) and the config dict + * This function will return a sync2jira.Intermediary.Issue object + * This function will be used when listening to the message bus + * :code:`def REPO-NAME_issues(upstream, config)` + * This function will take in an upstream repo name and the config dict + * This function will return a generator of sync2jira.Intermediary.Issue objects that contain all upstream Issues + * This function will be used to initialize and sync upstream/downstream issues +3. Now modify the `main.py` functions: + * :code:`def initialize(config)` + * Add another section (like Pagure and GitHub) to utlize the :code:`REPO-NAME_issues` function you just made. + * :code:`def listen(config)` + * Add another section to the if statement under Pagure and GitHub + * :code:`elif 'REPO-NAME' in suffix:` + * Now utilize the :code:`handle_REPO-NAME_message` function you just made +4. If all goes smoothly, your new repo should work with Sync2Jira! + +.. note:: If you want to submit a Pull Request, ensure that you add appropriate Unit Tests, Tox is passing, and you have appropriate documentation! diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..9db07fb --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = u'Sync2Jira' +copyright = u'2019, Ralph Bean' +author = u'Ralph Bean' + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'2.0' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['ntemplates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['nstatic'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +html_sidebars = { '**': ['globaltoc.html', 'searchbox.html'] } + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Sync2Jiradoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Sync2Jira.tex', u'Sync2Jira Documentation', + u'Ralph Bean', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'sync2jira', u'Sync2Jira Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Sync2Jira', u'Sync2Jira Documentation', + author, 'Sync2Jira', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- \ No newline at end of file diff --git a/docs/source/config-file.rst b/docs/source/config-file.rst new file mode 100644 index 0000000..4518daf --- /dev/null +++ b/docs/source/config-file.rst @@ -0,0 +1,108 @@ +Config File +=========== +The config file is made up of multiple parts + +.. code-block:: python + + 'admins': ['demo_jira_username'] + +* Admins can be users who manage Sync2Jira. They will be cc'd in any emails regarding duplicate issues found. + +.. code-block:: python + + 'initialize': True + +* Initilization set to True will ensure that there is an inital sync done when Sync2Jira starts. + It is recommended to leave this as True to ensure that all issues are in sync. + +.. code-block:: python + + 'testing': True + +* Testing is a flag that will determine if any changes are actually made downstream (on JIRA tickets). + Set to false if you are developing and don't want any changes to take effect. + +.. code-block:: python + + 'github_token': 'YOUR_TOKEN', + +* This is where you can enter your GitHub API token. + +.. code-block:: python + + 'default_jira_instance': 'example' + +* This is the default JIRA instance to be used if none is provided in the project. + +.. code-block:: python + + 'jira': { + 'example': { + 'options': { + 'server': 'https://some_jira_server_somewhere.com', + 'verify': True, + }, + 'basic_auth': ('YOU_USERNAME', 'YOUR_PASSWORD'), + }, + }, + +* Here you can configure multiple JIRA instances if you have projects with differing downstream JIRA instances. + Ensure to name them approproialty, in name of the JIRA instance above is `example`. + +.. code-block:: python + + 'map': { + 'pagure': { + 'Demo_project': {'project': 'FACTORY', 'component': 'gitbz', + 'updates': [...], 'owner': 'jira_username'}, + # 'koji': { 'project': 'BREW', 'component': None, }, + }, + 'github': { + 'GITHUB_USERNAME/Demo_project': {'project': 'FACTORY', 'component': 'gitbz', + 'updates': [...], 'owner': 'jira_username'}, + }, + }, + +* You can add your projects here. The 'project' field is associated with downstream JIRA projects, and 'component' with downstream components. + You can add the following to the updates array: + + * :code:`'comments'` + * Sync comments and comment edits + * :code:`{'tags': {'overwrite': True/False}}` + * Sync tags, do/don't overwrite downstream tags + * :code:`{'fixVersion'; {'overwrite': True/False}}` + * Sync fixVersion (downstream milestone), do/don't overwrite downstream fixVersion + * :code:`{'assignee': {'overwrite': True/False}}` + * Sync assignee (for Github only the first assignee will sync) do/don't overwrite downstream assignee + * :code:`'description'` + * Sync description + * :code:`'title'` + * Sync title + * :code:`{'transition': True/'CUSTOM_TRANSITION'}` + * Sync status (open/closed), Sync only status/Attempt to transition JIRA ticket to CUSTOM_TRANSITION on upstream closure + + .. note:: + + :Overwrite: Setting this to :code:`True` will ensure that Upstream (GitHub or Pagure) values will overwrite downstream ones (i.e. if its empty upstream it'll be empty downstream) + :CUSTOM_TRANSITION: Setting this value will get Sync2Jira to automatially transition downstream tickets once their upstream counterparts get closed. Set this to whatever 'closed' means downstream. + +* It is strongly encouraged for teams to use the :code:`owner` field. If configured, owners will be alerted if Sync2Jira finds dupliate downstream issues. + Further the owner will be used as a default in case the program is unable to find a valid assignee. + +.. code-block:: python + + 'filters': { + 'github': { + # Only sync multi-type tickets from bodhi. + 'fedora-infra/bodhi': {'state': 'open', 'milestone': 4, }, + }, + } + +* You can also add filters per-project. The following can be added to the filter dict: + + * :code:`status` + * Open/Closed + * :code:`tags` + * List of tags to look for + * :code:`milestone` + * Upstream milestone status \ No newline at end of file diff --git a/docs/source/downstream.rst b/docs/source/downstream.rst new file mode 100644 index 0000000..24b9275 --- /dev/null +++ b/docs/source/downstream.rst @@ -0,0 +1,8 @@ +Downstream +============ + +.. automodule:: sync2jira.downstream + :members: + :private-members: + + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..7d092e0 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,34 @@ +Sync2Jira documentation +===================================== + + +Home +^^^^^^^^^^^^^^^^^^ + +* :ref:`search` + + +.. toctree:: + :maxdepth: 2 + :caption: Setup Guide + + quickstart + config-file + + +.. toctree:: + :maxdepth: 1 + :caption: Adding New Repos + + adding-new-repo-guide + + +.. toctree:: + :maxdepth: 5 + :caption: Code Documentation + + main + upstream + downstream + intermediary + mailer \ No newline at end of file diff --git a/docs/source/intermediary.rst b/docs/source/intermediary.rst new file mode 100644 index 0000000..256bb21 --- /dev/null +++ b/docs/source/intermediary.rst @@ -0,0 +1,127 @@ +Intermediary +============ + +Sync2Jira converts upstream issues into custom Issue objects. You can see the __init__ below: + +.. code-block:: python + + def __init__(self, source, title, url, upstream, comments, + config, tags, fixVersion, priority, content, + reporter, assignee, status, id, downstream=None): + self.source = source + self._title = title + self.url = url + self.upstream = upstream + self.comments = comments + self.tags = tags + self.fixVersion = fixVersion + self.priority = priority + self.content = content + self.reporter = reporter + self.assignee = assignee + self.status = status + self.id = str(id) + if not downstream: + self.downstream = config['sync2jira']['map'][self.source][upstream] + else: + self.downstream = downstream + +The object has two properties: + +.. code-block:: python + + @property + def title(self): + return u'[%s] %s' % (self.upstream, self._title) + +* This will return the title in downstream form (i.e. [UPSTREAM_REPO] TITLE) + +.. code-block:: python + + @property + def upstream_title(self): + return self._title + +* This will return the raw title (i.e. TITLE) + +Currently the service is set up create Issue objects from Pagure and Github: + +.. code-block:: python + + @classmethod + def from_pagure(cls, upstream, issue, config): + base = config['sync2jira'].get('pagure_url', 'https://pagure.io') + comments = [] + for comment in issue['comments']: + # Only add comments that are not Metadata updates + if '**Metadata Update' in comment['comment']: + continue + # Else add the comment + # Convert the date to datetime + comment['date_created'] = datetime.fromtimestamp(float(comment['date_created'])) + comments.append({ + 'author': comment['user']['name'], + 'body': comment['comment'], + 'name': comment['user']['name'], + 'id': comment['id'], + 'date_created': comment['date_created'], + 'changed': None + }) + + return Issue( + source='pagure', + title=issue['title'], + url=base + '/%s/issue/%i' % (upstream, issue['id']), + upstream=upstream, + config=config, + comments=comments, + tags=issue['tags'], + fixVersion=[issue['milestone']], + priority=issue['priority'], + content=issue['content'], + reporter=issue['user'], + assignee=issue['assignee'], + status=issue['status'], + id=issue['date_created'] + ) + +.. code-block:: python + + @classmethod + def from_github(cls, upstream, issue, config): + comments = [] + for comment in issue['comments']: + comments.append({ + 'author': comment['author'], + 'name': comment['name'], + 'body': comment['body'], + 'id': comment['id'], + 'date_created': comment['date_created'], + 'changed': None + }) + + # Reformat the state field + if issue['state']: + if issue['state'] == 'open': + issue['state'] = 'Open' + elif issue['state'] == 'closed': + issue['state'] = 'Closed' + + # TODO: Priority is broken + return Issue( + source='github', + title=issue['title'], + url=issue['html_url'], + upstream=upstream, + config=config, + comments=comments, + tags=issue['labels'], + fixVersion=[issue['milestone']], + priority=None, + content=issue['body'], + reporter=issue['user'], + assignee=issue['assignees'], + status=issue['state'], + id=issue['id'] + ) +.. note:: Currently Priority is broken and is a known issue. diff --git a/docs/source/mailer.rst b/docs/source/mailer.rst new file mode 100644 index 0000000..b734b2a --- /dev/null +++ b/docs/source/mailer.rst @@ -0,0 +1,6 @@ +Mailer +======= + +.. automodule:: sync2jira.mailer + :members: + diff --git a/docs/source/main.rst b/docs/source/main.rst new file mode 100644 index 0000000..fc2abdf --- /dev/null +++ b/docs/source/main.rst @@ -0,0 +1,5 @@ +Main +==== + +.. automodule:: sync2jira.main + :members: diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 0000000..7e26900 --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,71 @@ +Quick Start +============ + +Want to quickly get started working with Sync2Jira? Follow these steps: + +1. **First open up** :code:`fedmsg.d/sync2jira.py` + +2. Enter your GitHub token which you can get `here `_ + .. code-block:: python + + 'github_token': 'YOUR_TOKEN', + +3. Enter relevent JIRA information + .. code-block:: python + + 'default_jira_instance': 'example', + 'jira': { + 'example': { + 'options': { + 'server': 'https://some_jira_server_somewhere.com', + 'verify': True, + }, + 'basic_auth': ('YOU_USERNAME', 'YOUR_PASSWORD'), + }, + }, + + .. note:: You might have to set verify to False + +4. Add your upstream repos to the `map` section + .. code-block:: python + + 'map': { + 'pagure': { + 'Demo_project': {'project': 'FACTORY', 'component': 'gitbz', + 'updates': [...]}, + # 'koji': { 'project': 'BREW', 'component': None, }, + }, + 'github': { + 'GITHUB_USERNAME/Demo_project': {'project': 'FACTORY', 'component': 'gitbz', + 'updates': [...]}, + }, + }, + + .. note:: You can learn more about what can go into the updates list `here `_ + +5. Finally you can tweak the config files optional settings to your liking + .. code-block:: python + + # Admins to be cc'd in duplicate emails + 'admins': ['demo_jira_username'], + # Scrape sources at startup + 'initialize': True, + # Don't actually make changes to JIRA... + 'testing': True, + + 'filters': { + 'github': { + # Only sync multi-type tickets from bodhi. + 'fedora-infra/bodhi': {'state': 'open', 'milestone': 4, }, + }, + } +6. Now that you're done with the config file you can install sync2jira and run + .. code-block:: shell + + python setup.py install + >> .... + >> Finished processing dependencies for sync2jira==1.7 + sync2jira + .. note:: You might have to add `config['validate_signatures'] = False`. + You can find out more under the `main `_. + diff --git a/docs/source/upstream.rst b/docs/source/upstream.rst new file mode 100644 index 0000000..2978ef8 --- /dev/null +++ b/docs/source/upstream.rst @@ -0,0 +1,5 @@ +Upstream +============ + +.. automodule:: sync2jira.upstream + :members: diff --git a/sync2jira/downstream.py b/sync2jira/downstream.py index cf86dd7..a74a63f 100644 --- a/sync2jira/downstream.py +++ b/sync2jira/downstream.py @@ -45,11 +45,11 @@ jira_cache = {} def _comment_format(comment): """ - Function to format JIRA comments - Args: - comment (dict): Upstream comment - Returns: - Response (str): Comments formatted + Function to format JIRA comments. + + :param dict comment: Upstream comment + :returns: Comments formatted + :rtype: String """ pretty_date = comment['date_created'].strftime("%a %b %d") return "[%s] Upstream, %s wrote [%s]:\n\n{quote}\n%s\n{quote}" % ( @@ -60,11 +60,11 @@ def _comment_format_legacy(comment): """ Legacy function to format JIRA comments. This is still used to match comments so no - duplicates are created - Args: - comment (dict): Upstream comment - Returns: - Response (str): Comments formatted + duplicates are created. + + :param dict comment: Upstream comment + :returns: Comments formatted + :rtype: String """ return "Upstream, %s wrote:\n\n{quote}\n%s\n{quote}" % ( comment['name'], comment['body']) @@ -72,12 +72,12 @@ def _comment_format_legacy(comment): def _get_jira_client(issue, config): """ - Function to match and create JIRA client - Args: - issue (sync2jira.intermediary.Issue): Issue object - config (dict): Config dict - Returns: - client (jira.client.JIRA): Matching JIRA client + Function to match and create JIRA client. + + :param sync2jira.intermediary.Issue issue: Issue object + :param dict config: Config dict + :returns: Matching JIRA client + :rtype: jira.client.JIRA """ # The name of the jira instance to use is stored under the 'map' # key in the config where each upstream is mapped to jira projects. @@ -104,14 +104,14 @@ def _get_jira_client(issue, config): def _matching_jira_issue_query(client, issue, config, free=False): """ - API calls that find matching JIRA tickets if any are present - Args: - client (jira.client.JIRA): JIRA client - issue (sync2jira.intermediary.Issue): Issue object - config (dict): Config dict - free (Bool): Free tag to add 'statusCategory != Done' to query - Returns: - results (lst): Returns a list of matching JIRA issues if any are found + API calls that find matching JIRA tickets if any are present. + + :param jira.client.JIRA client: JIRA client + :param sync2jira.intermediary.Issue issue: Issue object + :param Dict config: Config dict + :param Bool free: Free tag to add 'statusCategory != Done' to query + :returns: results: Returns a list of matching JIRA issues if any are found + :rtype: List """ # Searches for any remote link to the issue.url query = 'issueFunction in linkedIssuesOfRemote("%s") and ' \ @@ -177,15 +177,14 @@ def _matching_jira_issue_query(client, issue, config, free=False): def alert_user_of_duplicate_issues(issue, final_result, results_of_query, config, client): """ - Alerts owner of duplicate downstream issues - Args: - issue (sync2jira.intermediate.Issue): Upstream Issue object - final_result (list): Issue selected by matching algorithm - results_of_query (list): Result of JQL query - config (dict): Config dict - client (jira.client.JIRA): JIRA client - Returns: - Nothing + Alerts owner of duplicate downstream issues. + + :param sync2jira.intermediate.Issue issue: Upstream Issue object + :param List final_result: Issue selected by matching algorithm + :param List results_of_query: Result of JQL query + :param Dict config: Config dict + :param jira.client.JIRA client: JIRA client + :returns: Nothing """ # First remove final_result from results_of_query results_of_query.remove(final_result[0]) @@ -273,12 +272,12 @@ def alert_user_of_duplicate_issues(issue, final_result, results_of_query, def find_username(issue, config): """ - Finds JIRA username for an issue object - Args: - issue (sync2jira.intermediary.Issue): Issue object - config (dict): Config dict - Returns: - return (str): Username string + Finds JIRA username for an issue object. + + :param sync2jira.intermediary.Issue issue: Issue object + :param Dict config: Config dict + :returns: Username string + :rtype: String """ jira_instance = issue.downstream.get('jira_instance', False) if not jira_instance: @@ -292,16 +291,14 @@ def find_username(issue, config): def check_comments_for_duplicate(client, result, username): """ Checks comment of JIRA issue to see if it has been - marked as a duplicate - Args: - client (jira.client.JIRA): JIRA client) - result (jira.resource.Issue): JIRA issue - username (str): Username of JIRA user - Returns: - return (bool): True if duplicate comment was not found - *Or* - return (jira.resource.Issue): JIRA issue if we were able to - find it + marked as a duplicate. + + :param jira.client.JIRA client: JIRA client) + :param jira.resource.Issue result: JIRA issue + :param string username: Username of JIRA user + :returns: True if duplicate comment was not found or JIRA issue if \ + we were able to find it + :rtype: Bool or jira.resource.Issue """ for comment in client.comments(result): search = re.search(r'Marking as duplicate of (\w*)-(\d*)', @@ -314,12 +311,12 @@ def check_comments_for_duplicate(client, result, username): def _find_comment_in_jira(comment, j_comments): """ - Helper function to filter out comments that are matching - Args: - comment (dict): Individual comment from upstream - j_comments (lst(jira.resources.Comment)): Comments from JIRA downstream - Returns: - Item/None (jira.resource.Comment) + Helper function to filter out comments that are matching. + + :param Dict comment: Individual comment from upstream + :param List j_comments: Comments from JIRA downstream + :returns: Item/None + :rtype: jira.resource.Comment/None """ formatted_comment = _comment_format(comment) legacy_formatted_comment = _comment_format_legacy(comment) @@ -349,12 +346,12 @@ def _find_comment_in_jira(comment, j_comments): def _comment_matching(g_comments, j_comments): """ - Function to filter out comments that are matching - Args: - g_comments (lst(dict)): Comments from Issue object - j_comments (lst(jira.resources.Comment)): Comments from JIRA downstream - Returns: - response (lst): Returns a list of comments that are not matching + Function to filter out comments that are matching. + + :param List g_comments: Comments from Issue object + :param List j_comments: Comments from JIRA downstream + :returns: Returns a list of comments that are not matching + :rtype: List """ return list( filter( @@ -366,14 +363,14 @@ def _comment_matching(g_comments, j_comments): def _get_existing_jira_issue(client, issue, config): """ - Get a jira issue by the linked remote issue. + Get a jira issue by the linked remote issue. \ This is the new supported way of doing this. - Args: - client (jira.client.JIRA): JIRA client - issue (sync2jira.intermediary.Issue): Issue object - config (dict): Config dict - Returns: - response (lst): Returns a list of matching JIRA issues if any are found + + :param jira.client.JIRA client: JIRA client + :param sync2jira.intermediary.Issue issue: Issue object + :param Dict config: Config dict + :returns: Returns a list of matching JIRA issues if any are found + :rtype: List """ results = _matching_jira_issue_query(client, issue, config) if results: @@ -383,9 +380,10 @@ def _get_existing_jira_issue(client, issue, config): def _get_existing_jira_issue_legacy(client, issue, config): - """ This is our old way of matching issues: use the special url field. - + """ + This is our old way of matching issues: use the special url field. This will be phased out and removed in a future release. + """ kwargs = dict(issue.downstream.items()) @@ -405,17 +403,13 @@ def _get_existing_jira_issue_legacy(client, issue, config): def _attach_link(client, downstream, remote_link): """ - Attaches the upstream link to the JIRA ticket - Args: - client (jira.client.JIRA): JIRA client - downstream (jira.resources.Issue): Response from - creating the - JIRA ticket - remote_link(): - Returns: - downstream (jira.resources.Issue): Response from - creating the - JIRA ticket + Attaches the upstream link to the JIRA ticket. + + :param jira.client.JIRA client: JIRA client + :param jira.resources.Issue downstream: Response from creating the JIRA ticket + :param str remote_link: Remote link + :return: downstream: Response from creating the JIRA ticket + :rtype: jira.resources.Issue """ log.info(" Attaching tracking link %r to %r", remote_link, downstream.key) modified_desc = downstream.fields.description + " " @@ -438,9 +432,9 @@ def _attach_link(client, downstream, remote_link): def _upgrade_jira_issue(client, downstream, issue, config): - """ Given an old legacy-style downstream issue... + """ + Given an old legacy-style downstream issue... ...upgrade it to a new-style issue. - Simply mark it with an external-url field value. """ log.info(" Upgrading %r %r issue for %r", downstream.key, issue.downstream, issue) @@ -456,15 +450,13 @@ def _upgrade_jira_issue(client, downstream, issue, config): def assign_user(client, issue, downstream, remove_all=False): """ Attempts to assigns a JIRA issue to the correct - user based on the issue - Args: - client (jira.client.JIRA): JIRA Client - issue (sync2jira.intermediary.Issue): Issue object - downstream (jira.resources.Issue): JIRA issue object - remove_all (bool): Flag to indicate if we should reset - the assignees in the JIRA issue - Returns: - Nothing + user based on the issue. + + :param jira.client.JIRA client: JIRA Client + :param sync2jira.intermediary.Issue issue: Issue object + :param jira.resources.Issue downstream: JIRA issue object + :param Bool remove_all: Flag to indicate if we should reset the assignees in the JIRA issue + :returns: Nothing """ # If removeAll flag, then we need to reset the assignees if remove_all: @@ -500,14 +492,13 @@ def assign_user(client, issue, downstream, remove_all=False): def _create_jira_issue(client, issue, config): """ Create a JIRA issue and adds all relevant - information in the issue to the JIRA issue - Args: - client (jira.client.JIRA): JIRA client - issue (sync2jira.intermediary.Issue): Issue object - config (dict): Config dict - Returns: - downstream (jira.resources.Issue): Returns JIRA issue - that was created + information in the issue to the JIRA issue. + + :param jira.client.JIRA client: JIRA client + :param sync2jira.intermediary.Issue issue: Issue object + :param Dict config: Config dict + :returns: Returns JIRA issue that was created + :rtype: jira.resources.Issue """ log.info(" Creating %r issue for %r", issue.downstream, issue) if config['sync2jira']['testing']: @@ -560,12 +551,12 @@ def _create_jira_issue(client, issue, config): def _label_matching(jira_labels, issue_labels): """ Filters through jira_labels to ensure no duplicate labels are present and - no jira_labels are removed - Args: - jira_labels (lst(str)): Existing JIRA labels - issue_labels (lst(str): Upstream labels - Returns: - updated_labels (lst(str)): Updated filtered labels + no jira_labels are removed. + + :param List jira_labels: Existing JIRA labels + :param List issue_labels: Upstream labels + :returns: Updated filtered labels + :rtype: List """ # We want to get the union of the jira_labels and the issue_labels # i.e. all the labels in jira_labels and no duplicates from issue_labels @@ -576,13 +567,12 @@ def _label_matching(jira_labels, issue_labels): def _update_jira_issue(existing, issue, client): """ - Updates an existing JIRA issue (i.e. tags, assignee, comments etc) - Args: - existing (jira.resources.Issue): Existing JIRA issue that was found - issue (sync2jira.intermediary.Issue): Upstream issue we're pulling data from - client (jira.client.JIRA): JIRA Client - Returns: - Nothing + Updates an existing JIRA issue (i.e. tags, assignee, comments etc). + + :param jira.resources.Issue existing: Existing JIRA issue that was found + :param sync2jira.intermediary.Issue issue: Upstream issue we're pulling data from + :param jira.client.JIRA client: JIRA Client + :returns: Nothing """ # Start with comments # Only synchronize comments for listings that op-in @@ -638,13 +628,12 @@ def _update_jira_issue(existing, issue, client): def _update_transition(client, existing, issue): """ - Helper function to update the transition of a downstream JIRA issue - Args: - client (jira.client.JIRA): JIRA client - existing (jira.resource.Issue): Existing JIRA issue - issue (sync2jira.intermediary.Issue): Upstream issue - Returns: - Nothing + Helper function to update the transition of a downstream JIRA issue. + + :param jira.client.JIRA client: JIRA client + :param jira.resource.Issue existing: Existing JIRA issue + :param sync2jira.intermediary.Issue issue: Upstream issue + :returns: Nothing """ # Update the issue status in the JIRA description # Format the status @@ -702,12 +691,11 @@ def _update_transition(client, existing, issue): def _update_title(issue, existing): """ - Helper function to sync upstream/downstream title - Args: - issue (sync2jira.intermediary.Issue): Upstream issue - existing (jira.resource.Issue): Existing JIRA issue - Returns: - Nothing + Helper function to sync upstream/downstream title. + + :param sync2jira.intermediary.Issue issue: Upstream issue + :param jira.resource.Issue existing: Existing JIRA issue + :returns: Nothing """ new_description = existing.fields.description if not new_description: @@ -750,13 +738,12 @@ def _update_title(issue, existing): def _update_comments(client, existing, issue): """ - Helper function to sync comments between existing JIRA issue and upstream issue - Args: - client (jira.client.JIRA): JIRA client - existing (jira.resource.Issue): Existing JIRA issue - issue (sync2jira.intermediary.Issue): Upstream issue - Returns: - Nothing + Helper function to sync comments between existing JIRA issue and upstream issue. + + :param jira.client.JIRA client: JIRA client + :param jira.resource.Issue existing: Existing JIRA issue + :param sync2jira.intermediary.Issue issue: Upstream issue + :returns: Nothing """ # First get all existing comments comments = client.comments(existing) @@ -773,13 +760,12 @@ def _update_comments(client, existing, issue): def _update_fixVersion(updates, existing, issue): """ - Helper function to sync comments between existing JIRA issue and upstream issue - Args: - updates (list): Downstream updates requested by the user - existing (jira.resource.Issue): Existing JIRA issue - issue (sync2jira.intermediary.Issue): Upstream issue - Returns: - Nothing + Helper function to sync comments between existing JIRA issue and upstream issue. + + :param List updates: Downstream updates requested by the user + :param jira.resource.Issue existing: Existing JIRA issue + :param sync2jira.intermediary.Issue issue: Upstream issue + :returns: Nothing """ fix_version = [] # If we are not supposed to overwrite JIRA content @@ -827,14 +813,13 @@ def _update_fixVersion(updates, existing, issue): def _update_assignee(client, existing, issue, updates): """ - Helper function update existing JIRA assignee from downstream issue - Args: - client (jira.client.JIRA): JIRA client - existing (jira.resource.Issue): Existing JIRA issue - issue (sync2jira.intermediary.Issue): Upstream issue - updates (list): Downstream updates requested by the user - Returns: - Nothing + Helper function update existing JIRA assignee from downstream issue. + + :param jira.client.JIRA client: JIRA client + :param jira.resource.Issue existing: Existing JIRA issue + :param sync2jira.intermediary.Issue issue: Upstream issue + :param List updates: Downstream updates requested by the user + :returns: Nothing """ # First check if overwrite is set to True try: @@ -876,13 +861,12 @@ def _update_assignee(client, existing, issue, updates): def _update_tags(updates, existing, issue): """ - Helper function to sync tags between upstream issue and downstream JIRA issue - Args: - updates (list): Downstream updates requested by the user - existing (jira.resource.Issue): Existing JIRA issue - issue (sync2jira.intermediary.Issue): Upstream issue - Returns: - Nothing + Helper function to sync tags between upstream issue and downstream JIRA issue. + + :param List updates: Downstream updates requested by the user + :param jira.resource.Issue existing: Existing JIRA issue + :param sync2jira.intermediary.Issue issue: Upstream issue + :returns: Nothing """ # First get all existing tags on the issue updated_labels = issue.tags @@ -908,6 +892,13 @@ def _update_tags(updates, existing, issue): def _update_description(existing, issue): + """ + Helper function to sync description between upstream issue and downstream JIRA issue. + + :param jira.resource.Issue existing: Existing JIRA issue + :param sync2jira.intermediary.Issue issue: Upstream issue + :returns: Nothing + """ new_description = existing.fields.description if not new_description: new_description = '' @@ -973,11 +964,11 @@ def _update_description(existing, issue): def verify_tags(tags): """ - Helper function to ensure tag are JIRA ready :) - Args: - tags (lst(str)): Input tags - Returns: - updated_tags (lst(str)): Updates tags + Helper function to ensure tag are JIRA ready :). + + :param List tags: Input tags + :returns: Updates tags + :rtype: List """ updated_tags = [] for tag in tags: @@ -988,12 +979,11 @@ def verify_tags(tags): def sync_with_jira(issue, config): """ Attempts to sync a upstream issue with JIRA (i.e. by finding - an existing issue or creating a new one) - Args: - issue (sync2jira.intermediary.Issue): Issue object - config (dict): Config dict - Returns: - Nothing + an existing issue or creating a new one). + + :param sync2jira.intermediary.Issue issue: Issue object + :param Dict config: Config dict + :returns: Nothing """ log.info(" Considering upstream %s, %s", issue.url, issue.title) @@ -1035,6 +1025,15 @@ def sync_with_jira(issue, config): def _close_as_duplicate(client, duplicate, keeper, config): + """ + Helper function to close an issue as a duplicate. + + :param jira.client client: JIRA Client + :param jira.resources.Issue duplicate: Duplicate JIRA Issue + :param jira.resources.Issue keeper: JIRA issue to keep + :param Dict config: Config dict + :returns: Nothing + """ log.info(" Closing %s as duplicate of %s", duplicate.permalink(), keeper.permalink()) if config['sync2jira']['testing']: log.info(" Testing flag is true. Skipping actual delete.") @@ -1079,6 +1078,13 @@ def _close_as_duplicate(client, duplicate, keeper, config): def close_duplicates(issue, config): + """ + Function to close duplicate JIRA issues. + + :param sync2jira.intermediary.Issue issue: Upstream Issue + :param Dict config: Config dict + :returns: Nothing + """ # Create a client connection for this issue client = _get_jira_client(issue, config) diff --git a/sync2jira/mailer.py b/sync2jira/mailer.py index edbfed2..99a6627 100644 --- a/sync2jira/mailer.py +++ b/sync2jira/mailer.py @@ -13,12 +13,15 @@ DEFAULT_SERVER = os.environ['DEFAULT_SERVER'] def send_mail(recipients, subject, text, cc): - """ Sends email to recipients - :param list recipients : recipients of email - :param string subject : subject of the email - :param string text: HTML text - :param: list cc : cc of the email - :pram string text: text of the email + """ + Sends email to recipients. + + :param List recipients: recipients of email + :param String subject: subject of the email + :param String text: HTML text + :param String cc: cc of the email + :param String text: text of the email + :returns: Nothing """ _cfg = {} _cfg.setdefault("server", DEFAULT_SERVER) diff --git a/sync2jira/main.py b/sync2jira/main.py index 9c3148a..560145e 100644 --- a/sync2jira/main.py +++ b/sync2jira/main.py @@ -66,13 +66,12 @@ handlers = { def load_config(loader=fedmsg.config.load_config): """ - Generates and validates the config file - that will be used by fedmsg and JIRA client - Args: - loader (function): Function to set up runtime config - Returns: - config (dict): The config dict to be used - later in the program + Generates and validates the config file \ + that will be used by fedmsg and JIRA client. + + :param Function loader: Function to set up runtime config + :returns: The config dict to be used later in the program + :rtype: Dict """ config = loader() @@ -110,13 +109,12 @@ def load_config(loader=fedmsg.config.load_config): def listen(config): """ - Listens to activity on upstream repos on pagure and github - via fedmsg, and syncs new issues there to the JIRA instance + Listens to activity on upstream repos on pagure and github \ + via fedmsg, and syncs new issues there to the JIRA instance \ defined in 'fedmsg.d/sync2jira.py' - Args: - config (dict): Config dict - Returns: - Nothing + + :param Dict config: Config dict + :returns: Nothing """ if not config['sync2jira'].get('listen'): log.info("`listen` is disabled. Exiting.") @@ -145,14 +143,13 @@ def listen(config): def initialize(config): """ - Initial initialization needed to sync any upstream - repo with JIRA. Goes through all issues and - checks if they're already on JIRA / Need to be - created - Args: - config (dict): Config dict for JIRA - Returns: - Nothing + Initial initialization needed to sync any upstream \ + repo with JIRA. Goes through all issues and \ + checks if they're already on JIRA / Need to be \ + created. + + :param Dict config: Config dict for JIRA + :returns: Nothing """ log.info(" Running initialization to sync all issues from upstream to jira") log.info(" Testing flag is %r", config['sync2jira']['testing']) @@ -179,11 +176,9 @@ def initialize(config): def main(): """ Main function to check for initial sync - and listen for fedmgs - Args: - Nothing - Returns: - Nothing + and listen for fedmgs. + + :return: Nothing """ config = load_config() logging.basicConfig(level=logging.INFO) @@ -199,6 +194,11 @@ def main(): def list_managed(): + """ + Function to list URL for issues under map in config. + + :return: Nothing + """ config = load_config() mapping = config['sync2jira']['map'] warnings.simplefilter("ignore") @@ -213,6 +213,11 @@ def list_managed(): def close_duplicates(): + """ + Function to close duplicate functions. Uses downstream:close_duplicates. + + :return: Nothing + """ config = load_config() logging.basicConfig(level=logging.INFO) log.info(" Testing flag is %r", config['sync2jira']['testing']) diff --git a/sync2jira/upstream.py b/sync2jira/upstream.py index 3484cd6..081ef32 100644 --- a/sync2jira/upstream.py +++ b/sync2jira/upstream.py @@ -37,6 +37,14 @@ log = logging.getLogger(__name__) def handle_github_message(msg, config): + """ + Handle GitHub message from FedMsg. + + :param Dict msg: FedMsg Message + :param Dict config: Config File + :returns: Issue object + :rtype: sync2jira.intermediary.Issue + """ owner = msg['msg']['repository']['owner']['login'] repo = msg['msg']['repository']['name'] upstream = '{owner}/{repo}'.format(owner=owner, repo=repo) @@ -125,6 +133,14 @@ def handle_github_message(msg, config): def handle_pagure_message(msg, config): + """ + Handle Pagure message from FedMsg. + + :param Dict msg: FedMsg Message + :param Dict config: Config File + :returns: Issue object + :rtype: sync2jira.intermediary.Issue + """ upstream = msg['msg']['project']['name'] ns = msg['msg']['project'].get('namespace') or None if ns: @@ -194,6 +210,14 @@ def handle_pagure_message(msg, config): def pagure_issues(upstream, config): + """ + Creates a Generator for all Pagure issues in upstream repo. + + :param String upstream: Upstream Repo + :param Dict config: Config Dict + :returns: Pagure Issue object generator + :rtype: sync2jira.intermediary.Issue + """ base = config['sync2jira'].get('pagure_url', 'https://pagure.io') url = base + '/api/0/' + upstream + '/issues' @@ -224,6 +248,14 @@ def pagure_issues(upstream, config): def github_issues(upstream, config): + """ + Creates a Generator for all GitHub issues in upstream repo. + + :param String upstream: Upstream Repo + :param Dict config: Config Dict + :returns: Pagure Issue object generator + :rtype: sync2jira.intermediary.Issue + """ token = config['sync2jira'].get('github_token') if not token: headers = {} @@ -321,8 +353,9 @@ def _get_all_github_issues(url, headers): def _github_link_field_to_dict(field): - """ Utility for ripping apart github's Link header field. - It's kind of ugly. + """ + Utility for ripping apart github's Link header field. + It's kind of ugly. """ if not field: @@ -336,6 +369,9 @@ def _github_link_field_to_dict(field): def _fetch_github_data(url, headers): + """ + Helper function to gather GitHub data + """ response = requests.get(url, headers=headers) if not bool(response): try: