From a3ccf62045c9dc47eb83fa3f793846cf93b28d25 Mon Sep 17 00:00:00 2001 From: wwakabobik Date: Mon, 25 Sep 2023 15:50:11 +0200 Subject: [PATCH] PEP8 fixes + BLACK --- .pylintrc | 3 + __init__.py | 9 - __main__.py | 9 - pyproject.toml | 3 + setup.cfg | 3 + testrail_api_reporter/__init__.py | 2 + testrail_api_reporter/engines/__init__.py | 4 +- .../engines/at_coverage_reporter.py | 64 ++--- testrail_api_reporter/engines/case_backup.py | 35 ++- .../engines/plotly_reporter.py | 246 ++++++++++-------- .../engines/results_reporter.py | 40 +-- .../publishers/confluence_sender.py | 60 +++-- .../publishers/email_sender.py | 148 ++++++----- .../publishers/gdrive_uploader.py | 99 ++++--- .../publishers/slack_sender.py | 61 +++-- testrail_api_reporter/utils/case_stat.py | 65 ++++- testrail_api_reporter/utils/csv_parser.py | 48 ++-- testrail_api_reporter/utils/reporter_utils.py | 31 ++- 18 files changed, 562 insertions(+), 368 deletions(-) diff --git a/.pylintrc b/.pylintrc index ddcb06b..7344c0e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,3 +3,6 @@ max-line-length=120 [MESSAGES CONTROL] disable=E1101,R0913,W0718 + +[DESIGN] +max-attributes=10 diff --git a/__init__.py b/__init__.py index f8013df..e726325 100644 --- a/__init__.py +++ b/__init__.py @@ -1,13 +1,4 @@ """ This module is used to import all the classes and functions from the package """ # Engines -from .testrail_api_reporter.engines.at_coverage_reporter import ATCoverageReporter -from .testrail_api_reporter.engines.results_reporter import TestRailResultsReporter -from .testrail_api_reporter.engines.plotly_reporter import PlotlyReporter -from .testrail_api_reporter.engines.case_backup import TCBackup # Publishers -from .testrail_api_reporter.publishers.confluence_sender import ConfluenceSender -from .testrail_api_reporter.publishers.email_sender import EmailSender -from .testrail_api_reporter.publishers.slack_sender import SlackSender -from .testrail_api_reporter.publishers.gdrive_uploader import GoogleDriveUploader # Utils -from .testrail_api_reporter.utils.reporter_utils import upload_image, zip_file, delete_file diff --git a/__main__.py b/__main__.py index a9f9707..80aa362 100644 --- a/__main__.py +++ b/__main__.py @@ -1,13 +1,4 @@ """ TestRail API Reporter """ # Engines -from testrail_api_reporter.engines.at_coverage_reporter import ATCoverageReporter -from testrail_api_reporter.engines.results_reporter import TestRailResultsReporter -from testrail_api_reporter.engines.plotly_reporter import PlotlyReporter -from testrail_api_reporter.engines.case_backup import TCBackup # Publishers -from testrail_api_reporter.publishers.confluence_sender import ConfluenceSender -from testrail_api_reporter.publishers.email_sender import EmailSender -from testrail_api_reporter.publishers.slack_sender import SlackSender -from testrail_api_reporter.publishers.gdrive_uploader import GoogleDriveUploader # Utils -from testrail_api_reporter.utils.reporter_utils import upload_image, zip_file, delete_file diff --git a/pyproject.toml b/pyproject.toml index 6ce372c..5654b09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,9 @@ extend-ignore = """ [tool.pylint] max-line-length = 120 +[tool.pylint.'DESIGN'] +max-attributes = 15 + [tool.mypy] [[tool.mypy.overrides]] diff --git a/setup.cfg b/setup.cfg index d2dfd68..52f5600 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [flake8] max-line-length = 120 extend-ignore = Q000,WPS306,I001,I005,WPS229,D400,WPS317,S101,WPS507 + +[pylint] +max-attributes=15 diff --git a/testrail_api_reporter/__init__.py b/testrail_api_reporter/__init__.py index 674904d..fce936b 100644 --- a/testrail_api_reporter/__init__.py +++ b/testrail_api_reporter/__init__.py @@ -3,9 +3,11 @@ from .engines.at_coverage_reporter import ATCoverageReporter from .engines.plotly_reporter import PlotlyReporter from .engines.results_reporter import TestRailResultsReporter + # Publishers from .publishers.confluence_sender import ConfluenceSender from .publishers.email_sender import EmailSender from .publishers.slack_sender import SlackSender + # Utils from .utils.reporter_utils import upload_image, delete_file, zip_file diff --git a/testrail_api_reporter/engines/__init__.py b/testrail_api_reporter/engines/__init__.py index 1253d5b..78f1681 100644 --- a/testrail_api_reporter/engines/__init__.py +++ b/testrail_api_reporter/engines/__init__.py @@ -1,5 +1,5 @@ """ Engines package """ from .at_coverage_reporter import ATCoverageReporter -from .results_reporter import TestRailResultsReporter -from .plotly_reporter import PlotlyReporter from .case_backup import TCBackup +from .plotly_reporter import PlotlyReporter +from .results_reporter import TestRailResultsReporter diff --git a/testrail_api_reporter/engines/at_coverage_reporter.py b/testrail_api_reporter/engines/at_coverage_reporter.py index 692514c..7b54e48 100644 --- a/testrail_api_reporter/engines/at_coverage_reporter.py +++ b/testrail_api_reporter/engines/at_coverage_reporter.py @@ -13,16 +13,16 @@ class ATCoverageReporter: """ def __init__( - self, - url: str, - email: str, - password: str, - priority=None, - project=None, - type_platforms=None, - automation_platforms=None, - suite_id=None, - debug=None, + self, + url: str, + email: str, + password: str, + priority=None, + project=None, + type_platforms=None, + automation_platforms=None, + suite_id=None, + debug=None, ): """ General init @@ -114,13 +114,13 @@ def __get_all_sections(self, project_id=None, suite_id=None, debug=None): return sections def __get_all_cases( - self, - project_id=None, - suite_id=None, - section_id=None, - priority_id=None, - debug=None, - retries=3, + self, + project_id=None, + suite_id=None, + section_id=None, + priority_id=None, + debug=None, + retries=3, ): """ Wrapper to get all test cases for selected project, suite, section and priority @@ -205,13 +205,13 @@ def __get_all_cases( return cases_list def automation_state_report( - self, - priority=None, - project=None, - automation_platforms=None, - filename_pattern="current_automation", - suite=None, - debug=None, + self, + priority=None, + project=None, + automation_platforms=None, + filename_pattern="current_automation", + suite=None, + debug=None, ): """ Generates data of automation coverage for stacked bar chart or staked line chart @@ -265,9 +265,9 @@ def automation_state_report( results[index].set_automated(results[index].get_automated() + 1) else: if case[platform["internal_name"]] == platform["na_code"]: - results[index].set_na(results[index].get_na() + 1) + results[index].set_not_applicable(results[index].get_not_applicable() + 1) results[index].set_not_automated( - results[index].get_total() - results[index].get_automated() - results[index].get_na() + results[index].get_total() - results[index].get_automated() - results[index].get_not_applicable() ) # save history data filename = f"{filename_pattern}_{results[index].get_name().replace(' ', '_')}.csv" @@ -299,12 +299,12 @@ def test_case_by_priority(self, project=None, suite=None, debug=None): return results def test_case_by_type( - self, - project=None, - type_platforms=None, - filename_pattern="current_area_distribution", - suite=None, - debug=None, + self, + project=None, + type_platforms=None, + filename_pattern="current_area_distribution", + suite=None, + debug=None, ): """ Generates data for pie/line chart with distribution by type of platforms (guided by top section). diff --git a/testrail_api_reporter/engines/case_backup.py b/testrail_api_reporter/engines/case_backup.py index 4478990..6d06f05 100644 --- a/testrail_api_reporter/engines/case_backup.py +++ b/testrail_api_reporter/engines/case_backup.py @@ -6,10 +6,19 @@ class TCBackup: - """ TestRails backup class """ + """TestRails backup class""" - def __init__(self, test_rails_url, test_rails_username, test_rails_password, test_rails_suite, - cleanup_needed=True, backup_filename='backup.xml', cookie_name='cookie.txt', debug=True): + def __init__( + self, + test_rails_url, + test_rails_username, + test_rails_password, + test_rails_suite, + cleanup_needed=True, + backup_filename="backup.xml", + cookie_name="cookie.txt", + debug=True, + ): """ General init @@ -42,11 +51,13 @@ def __get_tr_cookie(self): :return: None """ if self.__debug: - print(f'\nGet cookie {self.__cookie_name} from {self.__url} for {self.__username}') - os.popen(f'curl -c {self.__cookie_name} ' - f'-H "Content-Type: application/x-www-form-urlencoded" ' - f'-d "name={self.__username}&password={self.__password}" -X POST ' - f'"{self.__url}/index.php?/auth/login"').read() + print(f"\nGet cookie {self.__cookie_name} from {self.__url} for {self.__username}") + os.popen( + f"curl -c {self.__cookie_name} " + f'-H "Content-Type: application/x-www-form-urlencoded" ' + f'-d "name={self.__username}&password={self.__password}" -X POST ' + f'"{self.__url}/index.php?/auth/login"' + ).read() def __download_tr_xml(self, filename=None, suite=None): """ @@ -61,10 +72,10 @@ def __download_tr_xml(self, filename=None, suite=None): if not suite: suite = self.__suite if self.__debug: - print(f'\nDownload XML {filename} from from {self.__url}') - os.popen(f'curl -b {self.__cookie_name} ' - f'"{self.__url}/index.php?/suites/export/{suite}" ' - f'--output {filename}').read() + print(f"\nDownload XML {filename} from from {self.__url}") + os.popen( + f"curl -b {self.__cookie_name} " f'"{self.__url}/index.php?/suites/export/{suite}" ' f"--output {filename}" + ).read() return filename def get_backup(self, filename=None, suite=None): diff --git a/testrail_api_reporter/engines/plotly_reporter.py b/testrail_api_reporter/engines/plotly_reporter.py index 0374e6a..1ffc034 100644 --- a/testrail_api_reporter/engines/plotly_reporter.py +++ b/testrail_api_reporter/engines/plotly_reporter.py @@ -4,11 +4,11 @@ from ..utils.csv_parser import CSVParser # Set path to orca for plotly -plotly.io.orca.config.executable = '/usr/local/bin/orca' +plotly.io.orca.config.executable = "/usr/local/bin/orca" class PlotlyReporter: - """ Class contains wrapper for generate reports (images) via plot charts """ + """Class contains wrapper for generate reports (images) via plot charts""" def __init__(self, pr_colors=None, pr_labels=None, ar_colors=None, lines=None, type_platforms=None, debug=True): """ @@ -27,12 +27,23 @@ def __init__(self, pr_colors=None, pr_labels=None, ar_colors=None, lines=None, t if not type_platforms: raise "Platform types is not provided, Plotly Reporter cannot be initialized!" self.__debug = debug - self.__pr_labels = pr_labels if pr_labels else ['Low', 'Medium', 'High', 'Critical'] - self.__pr_colors = pr_colors if pr_colors else ['rgb(173,216,230)', 'rgb(34,139,34)', 'rgb(255,255,51)', - 'rgb(255, 153, 153)'] - self.__ar_colors = ar_colors if ar_colors else ['rgb(255, 153, 153)', 'rgb(255,255,51)', 'rgb(34,139,34)', - 'rgb(173,216,230)', 'rgb(65,105,225)', 'rgb(192, 192, 192)'] - self.__lines = lines if lines else ({'color': 'rgb(0,0,51)', 'width': 1.5}) + self.__pr_labels = pr_labels if pr_labels else ["Low", "Medium", "High", "Critical"] + self.__pr_colors = ( + pr_colors if pr_colors else ["rgb(173,216,230)", "rgb(34,139,34)", "rgb(255,255,51)", "rgb(255, 153, 153)"] + ) + self.__ar_colors = ( + ar_colors + if ar_colors + else [ + "rgb(255, 153, 153)", + "rgb(255,255,51)", + "rgb(34,139,34)", + "rgb(173,216,230)", + "rgb(65,105,225)", + "rgb(192, 192, 192)", + ] + ) + self.__lines = lines if lines else ({"color": "rgb(0,0,51)", "width": 1.5}) self.__type_platforms = type_platforms def draw_automation_state_report(self, filename=None, reports=None, state_markers=None, debug=None): @@ -65,55 +76,70 @@ def draw_automation_state_report(self, filename=None, reports=None, state_marker axis_x.append(report.get_name()) axis_y_automated.append(report.get_automated()) axis_y_not_automated.append(report.get_not_automated()) - axis_y_na.append(report.get_na()) + axis_y_na.append(report.get_not_applicable()) if not state_markers: - state_markers = {'Automated': {'marker': dict(color='rgb(34,139,34)', - line=dict(color='rgb(0,0,51)', width=1.5)), - 'opacity': 0.6, 'textposition': 'auto'}, - 'Not automated': {'marker': dict(color='rgb(255, 153, 153)', - line=dict(color='rgb(0,0,51)', width=1.5)), - 'opacity': 0.6, 'textposition': 'auto'}, - 'N/A': {'marker': dict(color='rgb(192, 192, 192)', - line=dict(color='rgb(0,0,51)', width=1.5)), - 'opacity': 0.6, 'textposition': 'auto'}} + state_markers = { + "Automated": { + "marker": dict(color="rgb(34,139,34)", line=dict(color="rgb(0,0,51)", width=1.5)), + "opacity": 0.6, + "textposition": "auto", + }, + "Not automated": { + "marker": dict(color="rgb(255, 153, 153)", line=dict(color="rgb(0,0,51)", width=1.5)), + "opacity": 0.6, + "textposition": "auto", + }, + "N/A": { + "marker": dict(color="rgb(192, 192, 192)", line=dict(color="rgb(0,0,51)", width=1.5)), + "opacity": 0.6, + "textposition": "auto", + }, + } - data.append(plotly.graph_objs.Bar(x=axis_x, - y=axis_y_automated, - text=axis_y_automated, - name='Automated', - textposition=state_markers['Automated']['textposition'], - marker=state_markers['Automated']['marker'], - opacity=state_markers['Automated']['opacity'] - ) - ) - data.append(plotly.graph_objs.Bar(x=axis_x, - y=axis_y_not_automated, - text=axis_y_not_automated, - name='Not automated', - textposition=state_markers['Not automated']['textposition'], - marker=state_markers['Not automated']['marker'], - opacity=state_markers['Not automated']['opacity'] - ) - ) - data.append(plotly.graph_objs.Bar(x=axis_x, - y=axis_y_na, - text=axis_y_na, - name='N/A', - textposition=state_markers['N/A']['textposition'], - marker=state_markers['N/A']['marker'], - opacity=state_markers['N/A']['opacity'] - ) - ) + data.append( + plotly.graph_objs.Bar( + x=axis_x, + y=axis_y_automated, + text=axis_y_automated, + name="Automated", + textposition=state_markers["Automated"]["textposition"], + marker=state_markers["Automated"]["marker"], + opacity=state_markers["Automated"]["opacity"], + ) + ) + data.append( + plotly.graph_objs.Bar( + x=axis_x, + y=axis_y_not_automated, + text=axis_y_not_automated, + name="Not automated", + textposition=state_markers["Not automated"]["textposition"], + marker=state_markers["Not automated"]["marker"], + opacity=state_markers["Not automated"]["opacity"], + ) + ) + data.append( + plotly.graph_objs.Bar( + x=axis_x, + y=axis_y_na, + text=axis_y_na, + name="N/A", + textposition=state_markers["N/A"]["textposition"], + marker=state_markers["N/A"]["marker"], + opacity=state_markers["N/A"]["opacity"], + ) + ) - layout = plotly.graph_objs.Layout(barmode='stack') + layout = plotly.graph_objs.Layout(barmode="stack") if debug: - print(f'Drawing chart to file {filename}') + print(f"Drawing chart to file {filename}") fig = plotly.graph_objs.Figure(data=data, layout=layout) plotly.io.write_image(fig, filename) - def draw_test_case_by_priority(self, filename=None, values=None, pr_labels=None, pr_colors=None, - lines=None, debug=None): + def draw_test_case_by_priority( + self, filename=None, values=None, pr_labels=None, pr_colors=None, lines=None, debug=None + ): """ Generates an image file (png) with priority distribution (pie chart) @@ -135,22 +161,21 @@ def draw_test_case_by_priority(self, filename=None, values=None, pr_labels=None, debug = debug if debug is not None else self.__debug lines = lines if lines else self.__lines fig = { - 'data': [ + "data": [ { - 'values': values, - 'labels': pr_labels, - 'domain': {'column': 0}, - 'name': 'Test cases by priority', - 'hoverinfo': 'label+percent+name', - 'textinfo': 'value+percent', - 'type': 'pie', - 'marker': {'colors': pr_colors, - 'line': lines}, + "values": values, + "labels": pr_labels, + "domain": {"column": 0}, + "name": "Test cases by priority", + "hoverinfo": "label+percent+name", + "textinfo": "value+percent", + "type": "pie", + "marker": {"colors": pr_colors, "line": lines}, }, ] } if debug: - print(f'Drawing chart to file {filename}') + print(f"Drawing chart to file {filename}") plotly.io.write_image(fig, filename) def draw_test_case_by_area(self, filename=None, cases=None, ar_colors=None, lines=None, debug=None): @@ -181,29 +206,35 @@ def draw_test_case_by_area(self, filename=None, cases=None, ar_colors=None, line ar_values.append(case.get_total()) fig = { - 'data': [ + "data": [ { - 'values': ar_values, - 'labels': ar_labels, - 'domain': {'column': 0}, - 'name': 'Test cases by area', - 'hoverinfo': 'label+percent+name', - 'textinfo': 'value+percent', - 'type': 'pie', - 'marker': { - 'colors': ar_colors, - 'line': lines}, + "values": ar_values, + "labels": ar_labels, + "domain": {"column": 0}, + "name": "Test cases by area", + "hoverinfo": "label+percent+name", + "textinfo": "value+percent", + "type": "pie", + "marker": {"colors": ar_colors, "line": lines}, }, ] } if debug: - print(f'Drawing chart to file {filename}') + print(f"Drawing chart to file {filename}") plotly.io.write_image(fig, filename) - def draw_history_state_chart(self, chart_name: str, history_data=None, filename=None, trace1_decor=None, - trace2_decor=None, filename_pattern='current_automation', reverse_traces=False, - debug=None): + def draw_history_state_chart( + self, + chart_name: str, + history_data=None, + filename=None, + trace1_decor=None, + trace2_decor=None, + filename_pattern="current_automation", + reverse_traces=False, + debug=None, + ): """ Generates image file (png) with state distribution (staked line chart) @@ -225,28 +256,31 @@ def draw_history_state_chart(self, chart_name: str, history_data=None, filename= raise "No chart name is provided, report aborted!" debug = debug if debug is not None else self.__debug filename = filename if filename else f"{filename_pattern}_{chart_name.replace(' ', '_')}.csv" - trace1_decor = trace1_decor if trace1_decor else {'fill': 'tonexty', - 'line': dict(width=0.5, color='rgb(255, 153, 153)'), - 'mode': 'none'} - trace2_decor = trace2_decor if trace2_decor else {'fill': 'tozeroy', - 'line': dict(width=0.5, color='rgb(34,139,34)'), - 'mode': 'none'} + trace1_decor = ( + trace1_decor + if trace1_decor + else {"fill": "tonexty", "line": dict(width=0.5, color="rgb(255, 153, 153)"), "mode": "none"} + ) + trace2_decor = ( + trace2_decor + if trace2_decor + else {"fill": "tozeroy", "line": dict(width=0.5, color="rgb(34,139,34)"), "mode": "none"} + ) history_data = history_data if history_data else CSVParser(debug=debug, filename=filename).load_history_data() trace1 = plotly.graph_objs.Scatter( x=history_data[0], y=history_data[1], - fill=trace1_decor['fill'], - name='Total', - line=trace1_decor['line'], + fill=trace1_decor["fill"], + name="Total", + line=trace1_decor["line"], ) trace2 = plotly.graph_objs.Scatter( x=history_data[0], y=history_data[2], - - fill=trace2_decor['fill'], - name='Automated', - line=trace2_decor['line'], + fill=trace2_decor["fill"], + name="Automated", + line=trace2_decor["line"], ) fig = plotly.graph_objs.Figure() @@ -259,15 +293,21 @@ def draw_history_state_chart(self, chart_name: str, history_data=None, filename= fig.update_layout(yaxis=dict(nticks=30), autotypenumbers="convert types") fig.update_yaxes(range=[0, max([eval(i) for i in history_data[1]])]) - filename = f'{filename[:-3]}png' + filename = f"{filename[:-3]}png" if debug: - print(f'Drawing chart to file {filename}') + print(f"Drawing chart to file {filename}") plotly.io.write_image(fig, filename) return filename - def draw_history_type_chart(self, filename=None, type_platforms=None, - history_filename_pattern='current_area_distribution', ar_colors=None, lines=None, - debug=None): + def draw_history_type_chart( + self, + filename=None, + type_platforms=None, + history_filename_pattern="current_area_distribution", + ar_colors=None, + lines=None, + debug=None, + ): """ Generates an image file (png) with state distribution (staked line chart) @@ -289,17 +329,19 @@ def draw_history_type_chart(self, filename=None, type_platforms=None, debug = debug if debug is not None else self.__debug index = 0 for platform in type_platforms: - type_name = platform['name'] + type_name = platform["name"] history_filename = f"{history_filename_pattern}_{type_name.replace(' ', '_')}.csv" history_data = CSVParser(debug=debug, filename=history_filename).load_history_data() - data.append(plotly.graph_objs.Scatter( - x=history_data[0], - y=history_data[1], - name=type_name, - marker=dict(color=ar_colors[index], line=lines) - )) + data.append( + plotly.graph_objs.Scatter( + x=history_data[0], + y=history_data[1], + name=type_name, + marker=dict(color=ar_colors[index], line=lines), + ) + ) index += 1 - fig = {'data': data} + fig = {"data": data} if debug: - print(f'Drawing chart to file {filename}') + print(f"Drawing chart to file {filename}") plotly.io.write_image(fig, filename) diff --git a/testrail_api_reporter/engines/results_reporter.py b/testrail_api_reporter/engines/results_reporter.py index 40cf180..f8f0057 100644 --- a/testrail_api_reporter/engines/results_reporter.py +++ b/testrail_api_reporter/engines/results_reporter.py @@ -13,14 +13,14 @@ class TestRailResultsReporter: """Reporter to TestRails from xml report results, obtained by pytest""" def __init__( - self, - url: str, - email: str, - password: str, - project_id: int, - xml_report="junit-report.xml", - suite_id=None, - debug=True, + self, + url: str, + email: str, + password: str, + project_id: int, + xml_report="junit-report.xml", + suite_id=None, + debug=True, ): """ Default init @@ -289,15 +289,15 @@ def __prepare_title(self, environment=None, timestamp=None): return title def send_results( - self, - run_id=None, - environment=None, - title=None, - timestamp=None, - close_run=True, - run_name=None, - delete_old_run=False, - debug=None, + self, + run_id=None, + environment=None, + title=None, + timestamp=None, + close_run=True, + run_name=None, + delete_old_run=False, + debug=None, ): """ Send results to TestRail @@ -316,9 +316,9 @@ def send_results( if debug: print("\n") if ( - not self.__project_id - or not self.__at_section - or not self.__check_report_exists(xml_report=self.__xml_report) + not self.__project_id + or not self.__at_section + or not self.__check_report_exists(xml_report=self.__xml_report) ): print("Error! Please specify all required params!") self.__self_check() diff --git a/testrail_api_reporter/publishers/confluence_sender.py b/testrail_api_reporter/publishers/confluence_sender.py index 8c27c74..7f62817 100644 --- a/testrail_api_reporter/publishers/confluence_sender.py +++ b/testrail_api_reporter/publishers/confluence_sender.py @@ -5,12 +5,19 @@ class ConfluenceSender: - """ - Class contains wrapper for generate and send reports to Confluence - """ - - def __init__(self, url=None, username=None, password=None, confluence_page=None, automation_platforms=None, - type_platforms=None, plotly_engine=None, debug=True): + """Class contains wrapper for generate and send reports to Confluence""" + + def __init__( + self, + url=None, + username=None, + password=None, + confluence_page=None, + automation_platforms=None, + type_platforms=None, + plotly_engine=None, + debug=True, + ): """ General init @@ -38,7 +45,7 @@ def __init__(self, url=None, username=None, password=None, confluence_page=None, self.__automation_platforms = automation_platforms # should be passed with specific TestRails sections self.__type_platforms = type_platforms - def automation_state(self, confluence_page=None, reports=None, filename='current_automation.png', debug=None): + def automation_state(self, confluence_page=None, reports=None, filename="current_automation.png", debug=None): """ Generates and sends (attach) an image file (png) to confluence page with staked distribution (bar chart) with automation type coverage (or similar). @@ -56,10 +63,11 @@ def automation_state(self, confluence_page=None, reports=None, filename='current raise ValueError("No TestRail reports are provided, report aborted!") debug = debug if debug is not None else self.__debug self.__plotly.draw_automation_state_report(reports=reports, filename=filename, debug=debug) - self.__confluence.attach_file(filename, page_id=confluence_page, title='current_automation') + self.__confluence.attach_file(filename, page_id=confluence_page, title="current_automation") - def test_case_priority_distribution(self, confluence_page=None, values=None, - filename='current_priority_distribution.png', debug=None): + def test_case_priority_distribution( + self, confluence_page=None, values=None, filename="current_priority_distribution.png", debug=None + ): """ Generates and sends (attach) an image file (png) to confluence page with priority distribution (pie chart) @@ -76,10 +84,11 @@ def test_case_priority_distribution(self, confluence_page=None, values=None, raise ValueError("No TestRail reports are provided, report aborted!") debug = debug if debug is not None else self.__debug self.__plotly.draw_test_case_by_priority(values=values, filename=filename, debug=debug) - self.__confluence.attach_file(filename, page_id=confluence_page, title='current_priority_distribution') + self.__confluence.attach_file(filename, page_id=confluence_page, title="current_priority_distribution") - def test_case_area_distribution(self, confluence_page=None, cases=None, filename='current_area_distribution.png', - debug=None): + def test_case_area_distribution( + self, confluence_page=None, cases=None, filename="current_area_distribution.png", debug=None + ): """ Generates and sends (attach) an image file (png) to confluence page with sections distribution (pie chart) @@ -96,7 +105,7 @@ def test_case_area_distribution(self, confluence_page=None, cases=None, filename raise ValueError("No TestRail cases are provided, report aborted!") debug = debug if debug is not None else self.__debug self.__plotly.draw_test_case_by_area(cases=cases, filename=filename, debug=debug) - self.__confluence.attach_file(filename, page_id=confluence_page, title='current_area_distribution') + self.__confluence.attach_file(filename, page_id=confluence_page, title="current_area_distribution") def history_state_chart(self, confluence_page=None, automation_platforms=None, debug=None): """ @@ -118,11 +127,12 @@ def history_state_chart(self, confluence_page=None, automation_platforms=None, d for item in automation_platforms: if debug: print(f"generating chart for {item['name']}") - filename = self.__plotly.draw_history_state_chart(debug=debug, chart_name=item['name']) + filename = self.__plotly.draw_history_state_chart(debug=debug, chart_name=item["name"]) self.__confluence.attach_file(filename, page_id=confluence_page, title=filename[:-4]) - def history_type_chart(self, confluence_page=None, type_platforms=None, - filename='current_area_distribution_history.png', debug=None): + def history_type_chart( + self, confluence_page=None, type_platforms=None, filename="current_area_distribution_history.png", debug=None + ): """ Generates and sends (attach) an image file (png) to confluence page with state distribution (staked line chart) @@ -140,10 +150,18 @@ def history_type_chart(self, confluence_page=None, type_platforms=None, if not type_platforms: raise ValueError("No type platforms specified, report aborted!") self.__plotly.draw_history_type_chart(debug=debug, type_platforms=type_platforms, filename=filename) - self.__confluence.attach_file(filename, page_id=confluence_page, title='current_area_distribution_history') - - def generate_report(self, confluence_page=None, reports=None, cases=None, values=None, type_platforms=None, - automation_platforms=None, debug=None): + self.__confluence.attach_file(filename, page_id=confluence_page, title="current_area_distribution_history") + + def generate_report( + self, + confluence_page=None, + reports=None, + cases=None, + values=None, + type_platforms=None, + automation_platforms=None, + debug=None, + ): """ Generates and sends (attach) an image file (png) to confluence page with state distribution (staked line chart) diff --git a/testrail_api_reporter/publishers/email_sender.py b/testrail_api_reporter/publishers/email_sender.py index 42fc4a0..14e26d0 100644 --- a/testrail_api_reporter/publishers/email_sender.py +++ b/testrail_api_reporter/publishers/email_sender.py @@ -15,6 +15,8 @@ class EmailSender: + """Email sender class""" + def __init__(self, email=None, password=None, server_smtp=None, server_port=None, gmail_token=None, debug=True): """ General init @@ -31,13 +33,13 @@ def __init__(self, email=None, password=None, server_smtp=None, server_port=None self.__debug = debug self.__method = None if email is not None and password is not None and server_smtp is not None and server_port is not None: - self.__method = 'regular' + self.__method = "regular" elif gmail_token and email: - gmail_token = f'{os.getcwd()}/{gmail_token}' if not os.path.exists(gmail_token) else gmail_token + gmail_token = f"{os.getcwd()}/{gmail_token}" if not os.path.exists(gmail_token) else gmail_token if os.path.exists(gmail_token): - self.__method = 'gmail' - self.__gmail_scopes = 'https://www.googleapis.com/auth/gmail.send' - self.__gmail_app_name = 'Gmail API Python Send Email' + self.__method = "gmail" + self.__gmail_scopes = "https://www.googleapis.com/auth/gmail.send" + self.__gmail_app_name = "Gmail API Python Send Email" if not self.__method: raise ValueError("No email credentials are provided, aborted!") self.__email = email @@ -46,9 +48,19 @@ def __init__(self, email=None, password=None, server_smtp=None, server_port=None self.__server_port = server_port self.__gmail_token = gmail_token - def send_message(self, files=None, captions=None, image_width="400px", title=None, timestamp=None, recipients=None, - method=None, custom_message=None, - custom_folder=os.path.join(os.path.expanduser('~'), '.credentials'), debug=None): + def send_message( # pylint: disable=too-many-branches + self, + files=None, + captions=None, + image_width="400px", + title=None, + timestamp=None, + recipients=None, + method=None, + custom_message=None, + custom_folder=os.path.join(os.path.expanduser("~"), ".credentials"), + debug=None, + ): """ Send email to recipients with report (with attached images) @@ -80,25 +92,33 @@ def send_message(self, files=None, captions=None, image_width="400px", title=Non captions = None elif len(captions) != len(files): if debug: - print(f"Caption and file lists are not the same length {len(captions)} != {len(files)} thus " - f"no legend will be displayed") + print( + f"Caption and file lists are not the same length {len(captions)} != {len(files)} thus " + f"no legend will be displayed" + ) captions = None timestamp = timestamp if timestamp else datetime.now().strftime("%Y-%m-%d") title = title if title else f"Test development & automation coverage report for {timestamp}" # Connect and send message if not custom_message: - message = self.__prepare_payload(files=files, captions=captions, image_width=image_width, title=title, - recipients=recipients, method=method) + message = self.__prepare_payload( + files=files, + captions=captions, + image_width=image_width, + title=title, + recipients=recipients, + method=method, + ) else: if debug: print("Ignoring payload preparations, assuming user custom message is right") message = custom_message - if method == 'regular': + if method == "regular": connection = self.__connect_to_server() self.__send_to_server(connection=connection, recipients=recipients, message=message) self.__disconnect_from_server(connection=connection) - elif method == 'gmail': + elif method == "gmail": self.__gmail_send_message(message=message, custom_folder=custom_folder) if debug: print("Email sent!") @@ -110,14 +130,14 @@ def __connect_to_server(self): :return: connection handle ( smtplib.SMTP ) """ if self.__debug: - print(f'Connecting to custom mail server {self.__server_smtp}:{self.__server_port} using {self.__email}') + print(f"Connecting to custom mail server {self.__server_smtp}:{self.__server_port} using {self.__email}") try: connection = smtplib.SMTP(self.__server_smtp, self.__server_port) connection.ehlo() connection.starttls() connection.login(self.__email, self.__password) - except Exception as e: - raise ValueError(f"Can't login to mail!\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Can't login to mail!\nError{format_error(error)}") from error return connection def __send_to_server(self, connection, recipients, message): @@ -130,11 +150,11 @@ def __send_to_server(self, connection, recipients, message): :return: none """ if self.__debug: - print(f'Sending mail from {self.__email} to {recipients}') + print(f"Sending mail from {self.__email} to {recipients}") try: connection.sendmail(from_addr=self.__email, to_addrs=recipients, msg=message.as_string()) - except Exception as e: - raise ValueError(f"Can't send mail!\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Can't send mail!\nError{format_error(error)}") from error def __disconnect_from_server(self, connection): """ @@ -144,11 +164,11 @@ def __disconnect_from_server(self, connection): :return: none """ if self.__debug: - print(f'Disconnecting from custom server') + print(f"Disconnecting from custom server") try: connection.quit() - except Exception as e: - raise ValueError(f"Can't close connection!\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Can't close connection!\nError{format_error(error)}") from error def __prepare_payload(self, files, image_width, title, recipients, captions=None, method=None): """ @@ -162,32 +182,36 @@ def __prepare_payload(self, files, image_width, title, recipients, captions=None :param method: specify which method is used to set proper MIMEMultipart type ('gmail' or not) :return: formatted multipart message """ - message = MIMEMultipart("alternative") if method != 'gmail' else MIMEMultipart() + message = MIMEMultipart("alternative") if method != "gmail" else MIMEMultipart() message["Subject"] = title message["From"] = self.__email message["To"] = ", ".join(recipients) - html = '' \ - f'{title}' \ - f'

{title}' \ - '

>
' + ) + html = f"{html}
' + html = ( + '' + f"{title}" + f'

{title}' + '

{captions[j]}
' - html = f'{html}
>
' - html = f'{html}
' + ) for j, val in enumerate(files): - with open('{}'.format(val), "rb") as attachment: + with open("{}".format(val), "rb") as attachment: mime_image = MIMEImage(attachment.read()) # Define the image's ID with counter as you will reference it. - mime_image.add_header('Content-ID', f'') - mime_image.add_header('Content-Disposition', f'attachment; filename= {val}') + mime_image.add_header("Content-ID", f"") + mime_image.add_header("Content-Disposition", f"attachment; filename= {val}") message.attach(mime_image) # add to body if captions: html = f'{html}
' + html = ( + f'{html}
" message.attach(MIMEText(html, "html")) return message - def __gmail_get_credential_path(self, custom_folder=os.path.join(os.path.expanduser('~'), '.credentials')): + def __gmail_get_credential_path(self, custom_folder=os.path.join(os.path.expanduser("~"), ".credentials")): """ Service function target Google OAuth credentials path to storage @@ -200,13 +224,13 @@ def __gmail_get_credential_path(self, custom_folder=os.path.join(os.path.expandu if self.__debug: print(f"No credential directory found, creating new one here: {custom_folder}") os.makedirs(custom_folder, exist_ok=True) - except OSError as e: + except OSError as error: if self.__debug: - print(f"Original Error{format_error(e)}") - credential_path = os.path.join(custom_folder, 'gmail-python-email-send.json') + print(f"Original Error{format_error(error)}") + credential_path = os.path.join(custom_folder, "gmail-python-email-send.json") return credential_path - def __gmail_get_credentials(self, custom_folder=os.path.join(os.path.expanduser('~'), '.credentials')): + def __gmail_get_credentials(self, custom_folder=os.path.join(os.path.expanduser("~"), ".credentials")): """ Service function to get and convert Google OAuth credential from client_id and client_secret @@ -218,27 +242,31 @@ def __gmail_get_credentials(self, custom_folder=os.path.join(os.path.expanduser( print(f"Obtaining GMail credentials from {credential_path}") try: store = file.Storage(credential_path) - except Exception as e: - raise ValueError(f"Couldn't open storage\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Couldn't open storage\nError{format_error(error)}") from error try: credentials = store.get() - except Exception as e: - raise ValueError(f"Obtaining of credentials unexpectedly failed\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Obtaining of credentials unexpectedly failed\nError{format_error(error)}") from error if not credentials or credentials.invalid: try: flow = client.flow_from_clientsecrets(self.__gmail_token, self.__gmail_scopes) - except Exception as e: - raise ValueError(f"Couldn't obtain new client secrets from Google OAuth\nError{format_error(e)}") + except Exception as error: + raise ValueError( + f"Couldn't obtain new client secrets from Google OAuth\nError{format_error(error)}" + ) from error flow.user_agent = self.__gmail_app_name try: credentials = tools.run_flow(flow, store) - except Exception as e: - raise ValueError(f"Couldn't obtain new credential from Google OAuth\nError{format_error(e)}") + except Exception as error: + raise ValueError( + f"Couldn't obtain new credential from Google OAuth\nError{format_error(error)}" + ) from error if self.__debug: - print('Credentials stored to ' + credential_path) + print("Credentials stored to " + credential_path) return credentials - def __gmail_send_message(self, message, custom_folder=os.path.join(os.path.expanduser('~'), '.credentials')): + def __gmail_send_message(self, message, custom_folder=os.path.join(os.path.expanduser("~"), ".credentials")): """ Send Email via GMail @@ -251,17 +279,17 @@ def __gmail_send_message(self, message, custom_folder=os.path.join(os.path.expan credentials = self.__gmail_get_credentials(custom_folder=custom_folder) try: http = credentials.authorize(httplib2.Http()) - except Exception as e: - raise ValueError(f"Can't authorize via Google OAuth\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Can't authorize via Google OAuth\nError{format_error(error)}") from error try: - service = discovery.build('gmail', 'v1', http=http) - except Exception as e: - raise ValueError(f"Can't build service for Google OAuth\nError{format_error(e)}") + service = discovery.build("gmail", "v1", http=http) + except Exception as error: + raise ValueError(f"Can't build service for Google OAuth\nError{format_error(error)}") from error try: raw = base64.urlsafe_b64encode(message.as_bytes()).decode() - except Exception as e: - raise ValueError(f"Can't convert payload to base64\nError{format_error(e)}") - self.__gmail_send_message_internal(service, self.__email, {'raw': raw}) + except Exception as error: + raise ValueError(f"Can't convert payload to base64\nError{format_error(error)}") from error + self.__gmail_send_message_internal(service, self.__email, {"raw": raw}) def __gmail_send_message_internal(self, service, user_id, message): """ @@ -273,9 +301,9 @@ def __gmail_send_message_internal(self, service, user_id, message): :return: message """ try: - message = (service.users().messages().send(userId=user_id, body=message).execute()) + message = service.users().messages().send(userId=user_id, body=message).execute() if self.__debug: print(f'Message sent with Id: "{message["id"]}"!') return message - except Exception as e: - raise ValueError(f"Can't send mail via GMail!\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Can't send mail via GMail!\nError{format_error(error)}") from error diff --git a/testrail_api_reporter/publishers/gdrive_uploader.py b/testrail_api_reporter/publishers/gdrive_uploader.py index 35ef1e5..b271bdb 100644 --- a/testrail_api_reporter/publishers/gdrive_uploader.py +++ b/testrail_api_reporter/publishers/gdrive_uploader.py @@ -6,13 +6,22 @@ class GoogleDriveUploader: - """ Google Drive uploader class """ + """Google Drive uploader class""" + # Google token needs to be configured firstly, to do it, you have to visit: # https://console.developers.google.com/apis/credentials?pli=1 # Create Credentials => OAuth client ID => TV and limited Input Devices and get client_id and a client_secret # Then pass it as google_id = client_id and google_secret = client_secret - def __init__(self, google_id, google_secret, google_api_refresh_token=None, cleanup_needed=True, - backup_filename='backup.zip', mime_type='application/zip', debug=True): + def __init__( + self, + google_id, + google_secret, + google_api_refresh_token=None, + cleanup_needed=True, + backup_filename="backup.zip", + mime_type="application/zip", + debug=True, + ): """ General init @@ -36,7 +45,7 @@ def __init__(self, google_id, google_secret, google_api_refresh_token=None, clea self.__backup_filename = backup_filename self.__mime_type = mime_type - if not google_api_refresh_token or google_api_refresh_token == '': + if not google_api_refresh_token or google_api_refresh_token == "": self.__g_token, self.__g_refresh_token = self.__first_run() else: self.__g_refresh_token = google_api_refresh_token @@ -49,12 +58,16 @@ def __get_new_device_codes(self): """ if self.__debug: print("Get temporary Device ID and user code from Google Auth engine") - response = json.loads(os.popen(f'curl ' - f'-d "client_id={self.__g_id}&scope=https://www.googleapis.com/auth/drive.file"' - f' https://oauth2.googleapis.com/device/code').read()) + response = json.loads( + os.popen( + f"curl " + f'-d "client_id={self.__g_id}&scope=https://www.googleapis.com/auth/drive.file"' + f" https://oauth2.googleapis.com/device/code" + ).read() + ) if self.__debug: print(response) - return response['device_code'], response['user_code'], response['verification_url'] + return response["device_code"], response["user_code"], response["verification_url"] def __get_new_oauth_token(self, device_code): """ @@ -65,11 +78,15 @@ def __get_new_oauth_token(self, device_code): """ if self.__debug: print("Get OAuth token from google using Device ID") - response = json.loads(os.popen(f'curl -d client_id={self.__g_id} -d client_secret={self.__g_secret} ' - f'-d device_code={device_code} ' - f'-d grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code ' - f'https://accounts.google.com/o/oauth2/token').read()) - return response['access_token'], response['refresh_token'] + response = json.loads( + os.popen( + f"curl -d client_id={self.__g_id} -d client_secret={self.__g_secret} " + f"-d device_code={device_code} " + f"-d grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code " + f"https://accounts.google.com/o/oauth2/token" + ).read() + ) + return response["access_token"], response["refresh_token"] def __refresh_token(self): """ @@ -80,11 +97,15 @@ def __refresh_token(self): """ if self.__debug: print("Google OAuth token needs to be refreshed, so, let's do this") - response = json.loads(os.popen(f'curl -d client_id={self.__g_id} -d client_secret={self.__g_secret} ' - f'-d refresh_token={self.__g_refresh_token} ' - f'-d grant_type=refresh_token ' - f'https://accounts.google.com/o/oauth2/token').read()) - self.__g_token = response['access_token'] + response = json.loads( + os.popen( + f"curl -d client_id={self.__g_id} -d client_secret={self.__g_secret} " + f"-d refresh_token={self.__g_refresh_token} " + f"-d grant_type=refresh_token " + f"https://accounts.google.com/o/oauth2/token" + ).read() + ) + self.__g_token = response["access_token"] return self.__g_token def __first_run(self): @@ -102,18 +123,22 @@ def __first_run(self): print(f"Please fill device code {user_code} into web browser URL: {url}") input("When your code will be submitted and account, press enter") - print("Now you must ensure that your access rights are granted for this device! Proceed to:\n" - "https://console.developers.google.com/apis/api/drive.googleapis.com/overview\n" - "and open Credentials tab, now confirm OAuth API permissions for this device.\n" - "After submit please wait at least 5 minutes.") + print( + "Now you must ensure that your access rights are granted for this device! Proceed to:\n" + "https://console.developers.google.com/apis/api/drive.googleapis.com/overview\n" + "and open Credentials tab, now confirm OAuth API permissions for this device.\n" + "After submit please wait at least 5 minutes." + ) input("When 5 minutes passed, press any enter") access_token, refresh_token = self.__get_new_oauth_token(device_code=device_code) - print(f"Your access token is:\n{access_token}\nYour refresh token is:\n{refresh_token}\n" - f"Please save these credentials secure!\nYour access token will be valid for an 1 hour. " - f"If you plan use it in advance, you need to refresh it every hour or before use any time. \n" - f"Next time init this class with your refresh token to update access token automatically.") + print( + f"Your access token is:\n{access_token}\nYour refresh token is:\n{refresh_token}\n" + f"Please save these credentials secure!\nYour access token will be valid for an 1 hour. " + f"If you plan use it in advance, you need to refresh it every hour or before use any time. \n" + f"Next time init this class with your refresh token to update access token automatically." + ) return access_token, refresh_token @@ -129,16 +154,20 @@ def __upload_to_gdrive(self, filename=None, mime_type=None): if not mime_type: mime_type = self.__mime_type if self.__debug: - print(f'Uploading {filename} to GoogleDrive') - response = json.loads(os.popen(f'curl -X POST -L -H "Authorization: Bearer {self.__g_token}" ' - f'-F "metadata={{name :\'{filename.split(".")[0]}\'}};' - f'type=application/json;charset=UTF-8" ' - f'-F "file=@{filename};type={mime_type}" ' - f'"https://www.googleapis.com/upload/drive/v3/' - f'files?uploadType=multipart"').read()) - if response['id']: + print(f"Uploading {filename} to GoogleDrive") + response = json.loads( + os.popen( + f'curl -X POST -L -H "Authorization: Bearer {self.__g_token}" ' + f'-F "metadata={{name :\'{filename.split(".")[0]}\'}};' + f'type=application/json;charset=UTF-8" ' + f'-F "file=@{filename};type={mime_type}" ' + f'"https://www.googleapis.com/upload/drive/v3/' + f'files?uploadType=multipart"' + ).read() + ) + if response["id"]: if self.__debug: - print(f'Backup archive {filename} was uploaded to Google Drive') + print(f"Backup archive {filename} was uploaded to Google Drive") else: print("Something wrong, please check backup manually or re-run") diff --git a/testrail_api_reporter/publishers/slack_sender.py b/testrail_api_reporter/publishers/slack_sender.py index 43a963c..b669f4b 100644 --- a/testrail_api_reporter/publishers/slack_sender.py +++ b/testrail_api_reporter/publishers/slack_sender.py @@ -7,7 +7,7 @@ class SlackSender: - """ Slack sender class, see for details https://api.slack.com/messaging/webhooks """ + """Slack sender class, see for details https://api.slack.com/messaging/webhooks""" def __init__(self, hook_url=None, timeout=5, verify=True, debug=True): """ @@ -38,12 +38,14 @@ def __prepare_attachments(files, captions): """ legacy_attachments = [] for j, file in enumerate(files): - legacy_attachments.append({ - 'pretext': '----', - 'text': captions[j] if captions else '', - 'mrkdwn_in': ['text', 'pretext'], - "image_url": file, - }) + legacy_attachments.append( + { + "pretext": "----", + "text": captions[j] if captions else "", + "mrkdwn_in": ["text", "pretext"], + "image_url": file, + } + ) return legacy_attachments @staticmethod @@ -54,7 +56,7 @@ def __prepare_blocks(title): :param title: header title of message :return: list of dict with blocks info """ - return [{'type': 'header', 'text': {'type': 'plain_text', 'text': title, 'emoji': True}}] + return [{"type": "header", "text": {"type": "plain_text", "text": title, "emoji": True}}] def __prepare_payload(self, title, files, captions): """ @@ -65,8 +67,12 @@ def __prepare_payload(self, title, files, captions): :param captions: list of captions for files, list of strings, if not provided, no captions will be added :return: json with payload """ - return json.dumps({'attachments': self.__prepare_attachments(files=files, captions=captions), - 'blocks': self.__prepare_blocks(title=title)}) + return json.dumps( + { + "attachments": self.__prepare_attachments(files=files, captions=captions), + "blocks": self.__prepare_blocks(title=title), + } + ) @staticmethod def __prepare_headers(): @@ -75,10 +81,11 @@ def __prepare_headers(): :return: json with headers """ - return {'Content-type': 'application/json', 'Accept': 'text/plain'} + return {"Content-type": "application/json", "Accept": "text/plain"} - def send_message(self, files=None, captions=None, title=f"Test development & automation coverage report", - debug=None): + def send_message( + self, files=None, captions=None, title=f"Test development & automation coverage report", debug=None + ): """ Send message to Slack @@ -98,19 +105,27 @@ def send_message(self, files=None, captions=None, title=f"Test development & aut captions = None elif len(captions) != len(files): if debug: - print(f"Caption and file lists are not the same length {len(captions)} != {len(files)} thus " - f"no legend will be displayed") + print( + f"Caption and file lists are not the same length {len(captions)} != {len(files)} thus " + f"no legend will be displayed" + ) captions = None # Send to slack try: - response = requests.post(url=self.__hook_url, - data=self.__prepare_payload(title=title, files=files, captions=captions), - timeout=self.__timeout, verify=self.__verify, headers=self.__prepare_headers()) + response = requests.post( + url=self.__hook_url, + data=self.__prepare_payload(title=title, files=files, captions=captions), + timeout=self.__timeout, + verify=self.__verify, + headers=self.__prepare_headers(), + ) if response.status_code != 200: - raise ValueError(f"Message can't be sent! Error {response.status_code}: {response.text}: " - f"{response.raise_for_status()}") - elif debug: + raise ValueError( + f"Message can't be sent! Error {response.status_code}: {response.text}: " + f"{response.raise_for_status()}" + ) + if debug: print("Message sent!") - except Exception as e: - raise ValueError(f"Message can't be sent!\nError{format_error(e)}") + except Exception as error: + raise ValueError(f"Message can't be sent!\nError{format_error(error)}") diff --git a/testrail_api_reporter/utils/case_stat.py b/testrail_api_reporter/utils/case_stat.py index 0db6d8c..6ea9684 100644 --- a/testrail_api_reporter/utils/case_stat.py +++ b/testrail_api_reporter/utils/case_stat.py @@ -7,40 +7,95 @@ class CaseStat: """ def __init__(self, name): + """ + Constructor + + :param name: name of the test case + """ self.name = name self.total = 0 self.automated = 0 self.not_automated = 0 - self.na = 0 + self.not_applicable = 0 # getters def get_name(self): + """ + Returns the name of the test case + + :return: name of the test case + """ return self.name def get_total(self): + """ + Returns the total number of test cases + + :return: total number of test cases + """ return self.total def get_automated(self): + """ + Returns the number of automated test cases + + :return: number of automated test cases + """ return self.automated def get_not_automated(self): + """ + Returns the number of not automated test cases + + :return: number of not automated test cases + """ return self.not_automated - def get_na(self): - return self.na + def get_not_applicable(self): + """ + Returns the number of not applicable test cases + + :return: number of not applicable test cases + """ + return self.not_applicable # setters def set_name(self, name): + """ + Sets the name of the test case + + :param name: name of the test case + """ self.name = name def set_total(self, total): + """ + Sets the total number of test cases + + :param total: total number of test cases + """ self.total = total def set_automated(self, automated): + """ + Sets the number of automated test cases + + :param automated: number of automated test cases + """ self.automated = automated def set_not_automated(self, not_automated): + """ + Sets the number of not automated test cases + + :param not_automated: number of not automated test cases + """ self.not_automated = not_automated - def set_na(self, na): - self.na = na + def set_not_applicable(self, not_applicable): + """ + Sets the number of not applicable test cases + + :param not_applicable: number of not applicable test cases + """ + self.not_applicable = not_applicable diff --git a/testrail_api_reporter/utils/csv_parser.py b/testrail_api_reporter/utils/csv_parser.py index 8ea721b..03a60b3 100644 --- a/testrail_api_reporter/utils/csv_parser.py +++ b/testrail_api_reporter/utils/csv_parser.py @@ -5,7 +5,7 @@ class CSVParser: - """ Parser for CSV files """ + """Parser for CSV files""" def __init__(self, filename=None, debug=True): """ @@ -34,33 +34,37 @@ def save_history_data(self, filename=None, report=None, debug=None): raise ValueError("Filename for save report data is not provided, save history data aborted!") if not report: raise ValueError("Report couldn't be found, save history data aborted!") - date = datetime.today().strftime('%Y-%m-%d') - last_date = '' - mode = 'r' if exists(filename) else 'w' + date = datetime.today().strftime("%Y-%m-%d") + last_date = "" + mode = "r" if exists(filename) else "w" try: with open(filename, mode) as csvfile: - if mode == 'r': + if mode == "r": for row in reversed(list(csv.reader(csvfile))): - last_date = '{0}-{1}-{2}'.format(row[0], row[1], row[2]) + last_date = "{0}-{1}-{2}".format(row[0], row[1], row[2]) break except FileNotFoundError: - raise ValueError("Can't open report file, save history data aborted!") + raise ValueError("Can't open report file, save history data aborted!") from FileNotFoundError if last_date != date: if debug: - print('Saving data in {0} for {1}'.format(filename, date)) - with open(filename, 'a+', newline='') as csvfile: - writer = csv.writer(csvfile, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) + print("Saving data in {0} for {1}".format(filename, date)) + with open(filename, "a+", newline="") as csvfile: + writer = csv.writer(csvfile, delimiter=",", quotechar="|", quoting=csv.QUOTE_MINIMAL) - writer.writerow([datetime.today().strftime('%Y'), - datetime.today().strftime('%m'), - datetime.today().strftime('%d'), - report.get_total(), - report.get_automated(), - report.get_not_automated(), - report.get_na()]) + writer.writerow( + [ + datetime.today().strftime("%Y"), + datetime.today().strftime("%m"), + datetime.today().strftime("%d"), + report.get_total(), + report.get_automated(), + report.get_not_automated(), + report.get_not_applicable(), + ] + ) else: if debug: - print('Data already stored for today, skipping save') + print("Data already stored for today, skipping save") def load_history_data(self, filename=None, debug=None): """ @@ -80,15 +84,15 @@ def load_history_data(self, filename=None, debug=None): not_automated = [] nas = [] if debug: - print('Loading history data from {}'.format(filename)) + print("Loading history data from {}".format(filename)) try: - with open(filename, 'r') as csvfile: - for row in (csv.reader(csvfile)): + with open(filename, "r") as csvfile: + for row in csv.reader(csvfile): timestamps.append(datetime(year=int(row[0]), month=int(row[1]), day=int(row[2]))) totals.append(row[3]) automated.append(row[4]) not_automated.append(row[5]) nas.append(row[6]) except FileNotFoundError: - raise ValueError("Can't open report file, load history data aborted!") + raise ValueError("Can't open report file, load history data aborted!") from FileNotFoundError return [timestamps, totals, automated, not_automated, nas] diff --git a/testrail_api_reporter/utils/reporter_utils.py b/testrail_api_reporter/utils/reporter_utils.py index de2a4e3..e3380dd 100644 --- a/testrail_api_reporter/utils/reporter_utils.py +++ b/testrail_api_reporter/utils/reporter_utils.py @@ -11,10 +11,10 @@ def format_error(error): :param error: initial error :return: formatted string with error details """ - err_msg = '' + err_msg = "" error = error if isinstance(error, list) else [error] for err in error: - err_msg = f'{err_msg} : {err}' + err_msg = f"{err_msg} : {err}" return err_msg @@ -26,16 +26,15 @@ def upload_image(filename, api_token): :param api_token: unique API token for image upload on https://freeimage.host :return: dict with urls with image itself and its thumbnail """ - payload = { - 'type': 'file', - 'action': 'upload', - 'key': api_token + payload = {"type": "file", "action": "upload", "key": api_token} + files = {"source": open(filename, "rb")} + response = requests.post( + url="https://freeimage.host/api/1/upload", data=payload, timeout=5, verify=True, files=files + ) + return { + "image": response.json()["image"]["file"]["resource"]["chain"]["image"], + "thumb": response.json()["image"]["file"]["resource"]["chain"]["thumb"], } - files = {'source': open(filename, 'rb')} - response = requests.post(url='https://freeimage.host/api/1/upload', - data=payload, timeout=5, verify=True, files=files) - return {'image': response.json()['image']['file']['resource']['chain']['image'], - 'thumb': response.json()['image']['file']['resource']['chain']['thumb']} def delete_file(filename, debug=True): @@ -45,9 +44,9 @@ def delete_file(filename, debug=True): :param filename: filename or path to file, which should be deleted :param debug: debug output is enabled, may be True or False, optional, by default is True """ - os.popen(f'rm {filename}').read() + os.popen(f"rm {filename}").read() if debug: - print(f'Removed {filename}') + print(f"Removed {filename}") def zip_file(filename, suffix=None, debug=True): @@ -60,9 +59,9 @@ def zip_file(filename, suffix=None, debug=True): :return: zipped filename """ if suffix is None: - suffix = '' + suffix = "" zipped_file = f'{filename.split(".")[0]}{suffix}.zip' - os.popen(f'zip -r {zipped_file} {filename}').read() + os.popen(f"zip -r {zipped_file} {filename}").read() if debug: - print(f'ZIPped {filename} to {zipped_file}') + print(f"ZIPped {filename} to {zipped_file}") return zipped_file