From 66d8735c33e8f35b2d17a6d0e2dc3367680225db Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 10:59:22 -0500 Subject: [PATCH 1/9] Refactoring `action` to `validate` --- pypony.py | 2 +- src/__main__.py | 2 +- src/{action.py => validate.py} | 2 +- test/README.md | 2 +- test/{action_test.py => validate_test.py} | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{action.py => validate.py} (99%) rename test/{action_test.py => validate_test.py} (99%) diff --git a/pypony.py b/pypony.py index d027f8e..db81950 100644 --- a/pypony.py +++ b/pypony.py @@ -4,7 +4,7 @@ import click from actions_toolkit import core -from src.action import verify_api +from src.validate import verify_api @click.group() diff --git a/src/__main__.py b/src/__main__.py index e88f090..55a09b5 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -4,7 +4,7 @@ import click from actions_toolkit import core -from .action import verify_api +from .validate import verify_api @click.command() diff --git a/src/action.py b/src/validate.py similarity index 99% rename from src/action.py rename to src/validate.py index d349a0b..f25f36d 100644 --- a/src/action.py +++ b/src/validate.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""action.py +"""validate.py This module contains the primary code for the GitHub Action. Using the parsed OpenAPI specs and step files, it will create API requests and diff --git a/test/README.md b/test/README.md index 96aab01..ea85c86 100644 --- a/test/README.md +++ b/test/README.md @@ -5,7 +5,7 @@ ## Folder Structure - `/test` - - `action_test.py` + - `validate_test.py` - `...` - `/fixtures` (mock data) - `/specs` diff --git a/test/action_test.py b/test/validate_test.py similarity index 99% rename from test/action_test.py rename to test/validate_test.py index 5902b7e..d8a5d72 100644 --- a/test/action_test.py +++ b/test/validate_test.py @@ -5,7 +5,7 @@ import prance import pytest -from src.action import verify_api +from src.validate import verify_api from src.errors import ( UndocumentedEndpointError, InsufficientCoverageError, From 9b49e906b8038fa3814c7f1b544a5ec442c91f19 Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 11:00:13 -0500 Subject: [PATCH 2/9] Delete `__main__.py` Replaced with `./pypony.py` --- src/__main__.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 src/__main__.py diff --git a/src/__main__.py b/src/__main__.py deleted file mode 100644 index 55a09b5..0000000 --- a/src/__main__.py +++ /dev/null @@ -1,34 +0,0 @@ -import sys -import traceback - -import click -from actions_toolkit import core - -from .validate import verify_api - - -@click.command() -@click.option( - "--spec_file", required=True, type=click.STRING, envvar="INPUT_SPEC_FILE" -) -@click.option( - "--step_file", required=True, type=click.STRING, envvar="INPUT_STEP_FILE" -) -@click.option( - "-ff", "--fail-fast", default=False, type=click.BOOL, envvar="INPUT_FAIL_FAST" -) -@click.option( - "-v", "--verbose", default=False, type=click.BOOL, envvar="INPUT_VERBOSE" -) -def main(spec_file, step_file, fail_fast, verbose): - try: - verify_api(spec_file, step_file, fail_fast, verbose) - except BaseException as e: - if verbose: - core.error(traceback.format_exc(), title=e.__class__.__name__) - else: - core.error(str(e), title=e.__class__.__name__) - - sys.exit(1) - -main() From 459d39e6099250ee66b0dca1bea72461fb922d49 Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 11:06:12 -0500 Subject: [PATCH 3/9] PEP8 Spacing in `pypony.py` --- pypony.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pypony.py b/pypony.py index db81950..60650ad 100644 --- a/pypony.py +++ b/pypony.py @@ -11,6 +11,7 @@ def cli(): pass + @cli.command() @click.option( "--spec_file", required=True, type=click.STRING, envvar="INPUT_SPEC_FILE" @@ -35,4 +36,5 @@ def main(spec_file, step_file, fail_fast, verbose): sys.exit(1) + main() From d0ea2608b775714eac584ffb988e42521f7e546a Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 11:36:20 -0500 Subject: [PATCH 4/9] Add long description to `setup.py` Add rich dependency to requirements.txt --- requirements.txt | 1 + setup.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6c3e144..a64ec26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ py-openapi-schema-to-json-schema==0.0.3 python-dotenv==0.19.1 PyYAML==6.0 requests==2.26.0 +rich==11.2.0 actions-toolkit==0.0.9 diff --git a/setup.py b/setup.py index 2d62322..85d6350 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,8 @@ from setuptools import setup, find_packages +from pathlib import Path + +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() with open("requirements.txt", "r") as fp: requirements = fp.readlines() @@ -6,14 +10,16 @@ setup( name='PyPony', description='A python utility for contract testing APIs', + long_description=long_description, + long_description_content_type='text/markdown', author='Bandwidth', author_email='letstalk@bandwidth.com', url='https://github.com/Bandwidth/pypony/', version='0.1.0', py_modules=['pypony', 'src'], install_requires=requirements, - packages = find_packages(exclude=["website", "test"]), - entry_points = ''' + packages=find_packages(exclude=["website", "test"]), + entry_points=''' [console_scripts] pypony=pypony:cli ''' From f7c4a3f3d62cc87c67c88c9b611ceae46dcdc178 Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 11:46:13 -0500 Subject: [PATCH 5/9] Convert `-ff` and `-v` inputs to be true flags --- pypony.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pypony.py b/pypony.py index 60650ad..fb5b62e 100644 --- a/pypony.py +++ b/pypony.py @@ -20,10 +20,10 @@ def cli(): "--step_file", required=True, type=click.STRING, envvar="INPUT_STEP_FILE" ) @click.option( - "-ff", "--fail-fast", default=False, type=click.BOOL, envvar="INPUT_FAIL_FAST" + "-ff", "--fail-fast", is_flag=True, envvar="INPUT_FAIL_FAST" ) @click.option( - "-v", "--verbose", default=False, type=click.BOOL, envvar="INPUT_VERBOSE" + "-v", "--verbose", is_flag=True, envvar="INPUT_VERBOSE" ) def main(spec_file, step_file, fail_fast, verbose): try: From ddce65e3242c8d4a7e80476b69e9d70ccdd6671d Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 11:51:58 -0500 Subject: [PATCH 6/9] remove env var for `-v` and `-ff` flags --- pypony.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pypony.py b/pypony.py index fb5b62e..3ae4105 100644 --- a/pypony.py +++ b/pypony.py @@ -20,10 +20,10 @@ def cli(): "--step_file", required=True, type=click.STRING, envvar="INPUT_STEP_FILE" ) @click.option( - "-ff", "--fail-fast", is_flag=True, envvar="INPUT_FAIL_FAST" + "-ff", "--fail-fast", is_flag=True ) @click.option( - "-v", "--verbose", is_flag=True, envvar="INPUT_VERBOSE" + "-v", "--verbose", is_flag=True ) def main(spec_file, step_file, fail_fast, verbose): try: From 3639b54b08983bbbca95e52387e1b292560b108d Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:57:11 -0500 Subject: [PATCH 7/9] Convert some `core.info` statements to use rich `print` --- src/validate.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/validate.py b/src/validate.py index f25f36d..0012cda 100644 --- a/src/validate.py +++ b/src/validate.py @@ -10,6 +10,7 @@ import requests from actions_toolkit import core +from rich import print, print_json from dotenv import load_dotenv from jschon import create_catalog from jschon.jsonschema import OutputFormat @@ -104,7 +105,7 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b ResponseValidationError: If the response does not match the expected response according to schema """ - core.info('Validating APIs') + print('Validating APIs') # Create requests base_url: str = steps_data["base_url"] @@ -130,7 +131,7 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b try: # Get step name step_name = step_data.pop("name") - core.info(step_name) + print(step_name) # Create Request object path_url = step_data.pop("url") @@ -139,12 +140,13 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b # Evaluate expressions request.evaluate_all() - core.info(' Request:') - core.info(f' {request.method} {request.url}') - core.info(f' Authentication: {request.auth}') - core.info(f' Body: {request.body}') - core.info(f' Headers: {request.headers}') - core.info(f' Parameters: {request.params}') + print(' Request:') + print(f' {request.method} {request.url}') + print(f' Authentication: {request.auth}') + print(f' Body:') + print_json(data=request.body, indent=4) + print(f' Headers: {request.headers}') + print(f' Parameters: {request.params}') # Create Step object step = Step(step_name, request) @@ -163,10 +165,10 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b response = step.response - core.info(' Response:') - core.info(f' HTTP {response.status_code} {response.reason}') - core.info(f' Body: {response.body}') - core.info('') + print(' Response:') + print(f' HTTP {response.status_code} {response.reason}') + print_json(data=response.body, indent=4) + print('') status_code = step.response.status_code From f13f148301682d9d9ef482aca2392ace917b996b Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:30:08 -0500 Subject: [PATCH 8/9] Remove GH Actions and Coverage library usage Took 19 minutes --- .coveragerc | 7 - coverage/badge.svg | 1 - coverage/coverage_html.js | 575 ------------------ coverage/d_8e5d7c2a8ace5693_context_py.html | 173 ------ coverage/d_8e5d7c2a8ace5693_errors_py.html | 112 ---- coverage/d_8e5d7c2a8ace5693_request_py.html | 153 ----- coverage/d_8e5d7c2a8ace5693_response_py.html | 98 --- coverage/d_8e5d7c2a8ace5693_schema_py.html | 105 ---- coverage/d_8e5d7c2a8ace5693_singleton_py.html | 88 --- coverage/d_8e5d7c2a8ace5693_step_py.html | 107 ---- coverage/d_b4251882f9eb8989_action_py.html | 315 ---------- coverage/d_b4251882f9eb8989_errors_py.html | 162 ----- coverage/d_b4251882f9eb8989_parsing_py.html | 177 ------ .../d_b4251882f9eb8989_preprocessing_py.html | 127 ---- coverage/favicon_32.png | Bin 1732 -> 0 bytes coverage/index.html | 180 ------ coverage/index.js | 3 - coverage/keybd_closed.png | Bin 9004 -> 0 bytes coverage/keybd_open.png | Bin 9003 -> 0 bytes coverage/status.json | 1 - coverage/style.css | 307 ---------- requirements.dev.txt | 3 - requirements.txt | 1 - src/validate.py | 33 +- 24 files changed, 13 insertions(+), 2715 deletions(-) delete mode 100644 .coveragerc delete mode 100644 coverage/badge.svg delete mode 100644 coverage/coverage_html.js delete mode 100644 coverage/d_8e5d7c2a8ace5693_context_py.html delete mode 100644 coverage/d_8e5d7c2a8ace5693_errors_py.html delete mode 100644 coverage/d_8e5d7c2a8ace5693_request_py.html delete mode 100644 coverage/d_8e5d7c2a8ace5693_response_py.html delete mode 100644 coverage/d_8e5d7c2a8ace5693_schema_py.html delete mode 100644 coverage/d_8e5d7c2a8ace5693_singleton_py.html delete mode 100644 coverage/d_8e5d7c2a8ace5693_step_py.html delete mode 100644 coverage/d_b4251882f9eb8989_action_py.html delete mode 100644 coverage/d_b4251882f9eb8989_errors_py.html delete mode 100644 coverage/d_b4251882f9eb8989_parsing_py.html delete mode 100644 coverage/d_b4251882f9eb8989_preprocessing_py.html delete mode 100644 coverage/favicon_32.png delete mode 100644 coverage/index.html delete mode 100644 coverage/index.js delete mode 100644 coverage/keybd_closed.png delete mode 100644 coverage/keybd_open.png delete mode 100644 coverage/status.json delete mode 100644 coverage/style.css diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index dce6827..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -source= - src -omit= - venv/* - */__init__.py - */__main__.py diff --git a/coverage/badge.svg b/coverage/badge.svg deleted file mode 100644 index a43f6c6..0000000 --- a/coverage/badge.svg +++ /dev/null @@ -1 +0,0 @@ -coverage: 97.83%coverage97.83% \ No newline at end of file diff --git a/coverage/coverage_html.js b/coverage/coverage_html.js deleted file mode 100644 index 00e1848..0000000 --- a/coverage/coverage_html.js +++ /dev/null @@ -1,575 +0,0 @@ -// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -// Coverage.py HTML report browser code. -/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ -/*global coverage: true, document, window, $ */ - -coverage = {}; - -// General helpers -function debounce(callback, wait) { - let timeoutId = null; - return function(...args) { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - callback.apply(this, args); - }, wait); - }; -}; - -function checkVisible(element) { - const rect = element.getBoundingClientRect(); - const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); - const viewTop = 30; - return !(rect.bottom < viewTop || rect.top >= viewBottom); -} - -// Helpers for table sorting -function getCellValue(row, column = 0) { - const cell = row.cells[column] - if (cell.childElementCount == 1) { - const child = cell.firstElementChild - if (child instanceof HTMLTimeElement && child.dateTime) { - return child.dateTime - } else if (child instanceof HTMLDataElement && child.value) { - return child.value - } - } - return cell.innerText || cell.textContent; -} - -function rowComparator(rowA, rowB, column = 0) { - let valueA = getCellValue(rowA, column); - let valueB = getCellValue(rowB, column); - if (!isNaN(valueA) && !isNaN(valueB)) { - return valueA - valueB - } - return valueA.localeCompare(valueB, undefined, {numeric: true}); -} - -function sortColumn(th) { - // Get the current sorting direction of the selected header, - // clear state on other headers and then set the new sorting direction - const currentSortOrder = th.getAttribute("aria-sort"); - [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); - if (currentSortOrder === "none") { - th.setAttribute("aria-sort", th.dataset.defaultSortOrder || "ascending"); - } else { - th.setAttribute("aria-sort", currentSortOrder === "ascending" ? "descending" : "ascending"); - } - - const column = [...th.parentElement.cells].indexOf(th) - - // Sort all rows and afterwards append them in order to move them in the DOM - Array.from(th.closest("table").querySelectorAll("tbody tr")) - .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (th.getAttribute("aria-sort") === "ascending" ? 1 : -1)) - .forEach(tr => tr.parentElement.appendChild(tr) ); -} - -// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. -coverage.assign_shortkeys = function () { - document.querySelectorAll("[data-shortcut]").forEach(element => { - document.addEventListener("keypress", event => { - if (event.target.tagName.toLowerCase() === "input") { - return; // ignore keypress from search filter - } - if (event.key === element.dataset.shortcut) { - element.click(); - } - }); - }); -}; - -// Create the events for the filter box. -coverage.wire_up_filter = function () { - // Cache elements. - const table = document.querySelector("table.index"); - const table_body_rows = table.querySelectorAll("tbody tr"); - const no_rows = document.getElementById("no_rows"); - - // Observe filter keyevents. - document.getElementById("filter").addEventListener("input", debounce(event => { - // Keep running total of each metric, first index contains number of shown rows - const totals = new Array(table.rows[0].cells.length).fill(0); - // Accumulate the percentage as fraction - totals[totals.length - 1] = { "numer": 0, "denom": 0 }; - - // Hide / show elements. - table_body_rows.forEach(row => { - if (!row.cells[0].textContent.includes(event.target.value)) { - // hide - row.classList.add("hidden"); - return; - } - - // show - row.classList.remove("hidden"); - totals[0]++; - - for (let column = 1; column < totals.length; column++) { - // Accumulate dynamic totals - cell = row.cells[column] - if (column === totals.length - 1) { - // Last column contains percentage - const [numer, denom] = cell.dataset.ratio.split(" "); - totals[column]["numer"] += parseInt(numer, 10); - totals[column]["denom"] += parseInt(denom, 10); - } else { - totals[column] += parseInt(cell.textContent, 10); - } - } - }); - - // Show placeholder if no rows will be displayed. - if (!totals[0]) { - // Show placeholder, hide table. - no_rows.style.display = "block"; - table.style.display = "none"; - return; - } - - // Hide placeholder, show table. - no_rows.style.display = null; - table.style.display = null; - - const footer = table.tFoot.rows[0]; - // Calculate new dynamic sum values based on visible rows. - for (let column = 1; column < totals.length; column++) { - // Get footer cell element. - const cell = footer.cells[column]; - - // Set value into dynamic footer cell element. - if (column === totals.length - 1) { - // Percentage column uses the numerator and denominator, - // and adapts to the number of decimal places. - const match = /\.([0-9]+)/.exec(cell.textContent); - const places = match ? match[1].length : 0; - const { numer, denom } = totals[column]; - cell.dataset.ratio = `${numer} ${denom}`; - // Check denom to prevent NaN if filtered files contain no statements - cell.textContent = denom - ? `${(numer * 100 / denom).toFixed(places)}%` - : `${(100).toFixed(places)}%`; - } else { - cell.textContent = totals[column]; - } - } - })); - - // Trigger change event on setup, to force filter on page refresh - // (filter value may still be present). - document.getElementById("filter").dispatchEvent(new Event("change")); -}; - -coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; - -// Loaded on index.html -coverage.index_ready = function () { - coverage.assign_shortkeys(); - coverage.wire_up_filter(); - document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( - th => th.addEventListener("click", e => sortColumn(e.target)) - ); - - // Look for a localStorage item containing previous sort settings: - const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); - - if (stored_list) { - const {column, direction} = JSON.parse(stored_list); - const th = document.querySelector("[data-sortable]").tHead.rows[0].cells[column]; - th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); - th.click() - } - - // Watch for page unload events so we can save the final sort settings: - window.addEventListener("unload", function () { - const th = document.querySelector('[data-sortable] th[aria-sort="ascending"], [data-sortable] [aria-sort="descending"]'); - if (!th) { - return; - } - localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ - column: [...th.parentElement.cells].indexOf(th), - direction: th.getAttribute("aria-sort"), - })); - }); -}; - -// -- pyfile stuff -- - -coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; - -coverage.pyfile_ready = function () { - // If we're directed to a particular line number, highlight the line. - var frag = location.hash; - if (frag.length > 2 && frag[1] === 't') { - document.querySelector(frag).closest(".n").classList.add("highlight"); - coverage.set_sel(parseInt(frag.substr(2), 10)); - } else { - coverage.set_sel(0); - } - - const on_click = function(sel, fn) { - const elt = document.querySelector(sel); - if (elt) { - elt.addEventListener("click", fn); - } - } - on_click(".button_toggle_run", coverage.toggle_lines); - on_click(".button_toggle_mis", coverage.toggle_lines); - on_click(".button_toggle_exc", coverage.toggle_lines); - on_click(".button_toggle_par", coverage.toggle_lines); - - on_click(".button_next_chunk", coverage.to_next_chunk_nicely); - on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); - on_click(".button_top_of_page", coverage.to_top); - on_click(".button_first_chunk", coverage.to_first_chunk); - - coverage.filters = undefined; - try { - coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); - } catch(err) {} - - if (coverage.filters) { - coverage.filters = JSON.parse(coverage.filters); - } - else { - coverage.filters = {run: false, exc: true, mis: true, par: true}; - } - - for (cls in coverage.filters) { - coverage.set_line_visibilty(cls, coverage.filters[cls]); - } - - coverage.assign_shortkeys(); - coverage.init_scroll_markers(); - coverage.wire_up_sticky_header(); - - // Rebuild scroll markers when the window height changes. - window.addEventListener("resize", coverage.build_scroll_markers); -}; - -coverage.toggle_lines = function (event) { - const btn = event.target.closest("button"); - const category = btn.value - const show = !btn.classList.contains("show_" + category); - coverage.set_line_visibilty(category, show); - coverage.build_scroll_markers(); - coverage.filters[category] = show; - try { - localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); - } catch(err) {} -}; - -coverage.set_line_visibilty = function (category, should_show) { - const cls = "show_" + category; - const btn = document.querySelector(".button_toggle_" + category); - if (btn) { - if (should_show) { - document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); - btn.classList.add(cls); - } - else { - document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); - btn.classList.remove(cls); - } - } -}; - -// Return the nth line div. -coverage.line_elt = function (n) { - return document.getElementById("t" + n)?.closest("p"); -}; - -// Set the selection. b and e are line numbers. -coverage.set_sel = function (b, e) { - // The first line selected. - coverage.sel_begin = b; - // The next line not selected. - coverage.sel_end = (e === undefined) ? b+1 : e; -}; - -coverage.to_top = function () { - coverage.set_sel(0, 1); - coverage.scroll_window(0); -}; - -coverage.to_first_chunk = function () { - coverage.set_sel(0, 1); - coverage.to_next_chunk(); -}; - -// Return a string indicating what kind of chunk this line belongs to, -// or null if not a chunk. -coverage.chunk_indicator = function (line_elt) { - const classes = line_elt?.className; - if (!classes) { - return null; - } - const match = classes.match(/\bshow_\w+\b/); - if (!match) { - return null; - } - return match[0]; -}; - -coverage.to_next_chunk = function () { - const c = coverage; - - // Find the start of the next colored chunk. - var probe = c.sel_end; - var chunk_indicator, probe_line; - while (true) { - probe_line = c.line_elt(probe); - if (!probe_line) { - return; - } - chunk_indicator = c.chunk_indicator(probe_line); - if (chunk_indicator) { - break; - } - probe++; - } - - // There's a next chunk, `probe` points to it. - var begin = probe; - - // Find the end of this chunk. - var next_indicator = chunk_indicator; - while (next_indicator === chunk_indicator) { - probe++; - probe_line = c.line_elt(probe); - next_indicator = c.chunk_indicator(probe_line); - } - c.set_sel(begin, probe); - c.show_selection(); -}; - -coverage.to_prev_chunk = function () { - const c = coverage; - - // Find the end of the prev colored chunk. - var probe = c.sel_begin-1; - var probe_line = c.line_elt(probe); - if (!probe_line) { - return; - } - var chunk_indicator = c.chunk_indicator(probe_line); - while (probe > 1 && !chunk_indicator) { - probe--; - probe_line = c.line_elt(probe); - if (!probe_line) { - return; - } - chunk_indicator = c.chunk_indicator(probe_line); - } - - // There's a prev chunk, `probe` points to its last line. - var end = probe+1; - - // Find the beginning of this chunk. - var prev_indicator = chunk_indicator; - while (prev_indicator === chunk_indicator) { - probe--; - if (probe <= 0) { - return; - } - probe_line = c.line_elt(probe); - prev_indicator = c.chunk_indicator(probe_line); - } - c.set_sel(probe+1, end); - c.show_selection(); -}; - -// Returns 0, 1, or 2: how many of the two ends of the selection are on -// the screen right now? -coverage.selection_ends_on_screen = function () { - if (coverage.sel_begin === 0) { - return 0; - } - - const begin = coverage.line_elt(coverage.sel_begin); - const end = coverage.line_elt(coverage.sel_end-1); - - return ( - (checkVisible(begin) ? 1 : 0) - + (checkVisible(end) ? 1 : 0) - ); -}; - -coverage.to_next_chunk_nicely = function () { - if (coverage.selection_ends_on_screen() === 0) { - // The selection is entirely off the screen: - // Set the top line on the screen as selection. - - // This will select the top-left of the viewport - // As this is most likely the span with the line number we take the parent - const line = document.elementFromPoint(0, 0).parentElement; - if (line.parentElement !== document.getElementById("source")) { - // The element is not a source line but the header or similar - coverage.select_line_or_chunk(1); - } else { - // We extract the line number from the id - coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); - } - } - coverage.to_next_chunk(); -}; - -coverage.to_prev_chunk_nicely = function () { - if (coverage.selection_ends_on_screen() === 0) { - // The selection is entirely off the screen: - // Set the lowest line on the screen as selection. - - // This will select the bottom-left of the viewport - // As this is most likely the span with the line number we take the parent - const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; - if (line.parentElement !== document.getElementById("source")) { - // The element is not a source line but the header or similar - coverage.select_line_or_chunk(coverage.lines_len); - } else { - // We extract the line number from the id - coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); - } - } - coverage.to_prev_chunk(); -}; - -// Select line number lineno, or if it is in a colored chunk, select the -// entire chunk -coverage.select_line_or_chunk = function (lineno) { - var c = coverage; - var probe_line = c.line_elt(lineno); - if (!probe_line) { - return; - } - var the_indicator = c.chunk_indicator(probe_line); - if (the_indicator) { - // The line is in a highlighted chunk. - // Search backward for the first line. - var probe = lineno; - var indicator = the_indicator; - while (probe > 0 && indicator === the_indicator) { - probe--; - probe_line = c.line_elt(probe); - if (!probe_line) { - break; - } - indicator = c.chunk_indicator(probe_line); - } - var begin = probe + 1; - - // Search forward for the last line. - probe = lineno; - indicator = the_indicator; - while (indicator === the_indicator) { - probe++; - probe_line = c.line_elt(probe); - indicator = c.chunk_indicator(probe_line); - } - - coverage.set_sel(begin, probe); - } - else { - coverage.set_sel(lineno); - } -}; - -coverage.show_selection = function () { - // Highlight the lines in the chunk - document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); - for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { - coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); - } - - coverage.scroll_to_selection(); -}; - -coverage.scroll_to_selection = function () { - // Scroll the page if the chunk isn't fully visible. - if (coverage.selection_ends_on_screen() < 2) { - const element = coverage.line_elt(coverage.sel_begin); - coverage.scroll_window(element.offsetTop - 60); - } -}; - -coverage.scroll_window = function (to_pos) { - window.scroll({top: to_pos, behavior: "smooth"}); -}; - -coverage.init_scroll_markers = function () { - // Init some variables - coverage.lines_len = document.querySelectorAll('#source > p').length; - - // Build html - coverage.build_scroll_markers(); -}; - -coverage.build_scroll_markers = function () { - const temp_scroll_marker = document.getElementById('scroll_marker') - if (temp_scroll_marker) temp_scroll_marker.remove(); - // Don't build markers if the window has no scroll bar. - if (document.body.scrollHeight <= window.innerHeight) { - return; - } - - const marker_scale = window.innerHeight / document.body.scrollHeight; - const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); - - let previous_line = -99, last_mark, last_top; - - const scroll_marker = document.createElement("div"); - scroll_marker.id = "scroll_marker"; - document.getElementById('source').querySelectorAll( - 'p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par' - ).forEach(element => { - const line_top = Math.floor(element.offsetTop * marker_scale); - const line_number = parseInt(element.id.substr(1)); - - if (line_number === previous_line + 1) { - // If this solid missed block just make previous mark higher. - last_mark.style.height = `${line_top + line_height - last_top}px`; - } else { - // Add colored line in scroll_marker block. - last_mark = document.createElement("div"); - last_mark.id = `m${line_number}`; - last_mark.classList.add("marker"); - last_mark.style.height = `${line_height}px`; - last_mark.style.top = `${line_top}px`; - scroll_marker.append(last_mark); - last_top = line_top; - } - - previous_line = line_number; - }); - - // Append last to prevent layout calculation - document.body.append(scroll_marker); -}; - -coverage.wire_up_sticky_header = function () { - const header = document.querySelector('header'); - const header_bottom = ( - header.querySelector('.content h2').getBoundingClientRect().top - - header.getBoundingClientRect().top - ); - - function updateHeader() { - if (window.scrollY > header_bottom) { - header.classList.add('sticky'); - } else { - header.classList.remove('sticky'); - } - } - - window.addEventListener('scroll', updateHeader); - updateHeader(); -}; - -document.addEventListener("DOMContentLoaded", () => { - if (document.body.classList.contains("indexfile")) { - coverage.index_ready(); - } else { - coverage.pyfile_ready(); - } -}); diff --git a/coverage/d_8e5d7c2a8ace5693_context_py.html b/coverage/d_8e5d7c2a8ace5693_context_py.html deleted file mode 100644 index 220a602..0000000 --- a/coverage/d_8e5d7c2a8ace5693_context_py.html +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - Coverage for api_validator/models/context.py: 94% - - - - - -
-
-

- Coverage for api_validator/models/context.py: - 94% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 42 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""context.py 

-

3 

-

4Context manages all variables used during validation, 

-

5like system environment variables and responses of previous steps. 

-

6""" 

-

7 

-

8import os 

-

9import re 

-

10from types import SimpleNamespace 

-

11 

-

12from .singleton import Singleton 

-

13from .errors import BaseContextError, EvaluationError, EnvironmentVariableError 

-

14 

-

15 

-

16class Context(metaclass=Singleton): 

-

17 """ 

-

18 The global context object will be used as a singleton across the entire system. 

-

19 """ 

-

20 

-

21 _steps = SimpleNamespace() 

-

22 

-

23 @property 

-

24 def steps(self): 

-

25 return self._steps 

-

26 

-

27 def add_steps(self, step): 

-

28 """ 

-

29 Adds a Step object as an attribute of `self.steps` 

-

30 

-

31 Args: 

-

32 step (Step): the Step object to add 

-

33 """ 

-

34 

-

35 setattr(self.steps, step.name, step) 

-

36 

-

37 def clear_steps(self): 

-

38 """ 

-

39 Clears all Steps objects from attributes of `self.steps` 

-

40 """ 

-

41 

-

42 self._steps = SimpleNamespace() 

-

43 

-

44 # noinspection PyMethodMayBeStatic 

-

45 def evaluate(self, expression: any) -> any: 

-

46 """ 

-

47 Recursively evaluate nested expressions using depth-first search. 

-

48 Eventually the evaluation result as a string is returned. 

-

49 

-

50 The only allowed base contexts are "env" and "steps". 

-

51 

-

52 Args: 

-

53 expression (str): Object of any type that may contain expression(s) 

-

54 

-

55 Raises: 

-

56 EnvironmentVariableError: 

-

57 if the expression represents an environment variable but it cannot be found 

-

58 

-

59 Returns: 

-

60 The evaluated result as a string if there is any expression, original value otherwise. 

-

61 """ 

-

62 

-

63 if expression is None: 63 ↛ 64line 63 didn't jump to line 64, because the condition on line 63 was never true

-

64 return 

-

65 

-

66 # Evaluate each value in a dictionary 

-

67 if isinstance(expression, dict): 

-

68 return dict(map(lambda x: (x[0], self.evaluate(x[1])), expression.items())) 

-

69 

-

70 # Evaluate each element in a list 

-

71 if isinstance(expression, list): 

-

72 return list(map(lambda x: self.evaluate(x), expression)) 

-

73 

-

74 if not isinstance(expression, str): 

-

75 return expression 

-

76 

-

77 matches: list[str] = re.findall(r"(\${{[^/}]*}})", expression) 

-

78 if not matches: 

-

79 return expression 

-

80 

-

81 for match in matches: 

-

82 value = match.removeprefix("${{").removesuffix("}}").strip() 

-

83 base = value.split(".").pop(0) 

-

84 

-

85 if base == "env": 

-

86 # Only split at the first dot 

-

87 result = os.environ.get(value.split(".", 1)[1]) 

-

88 if result is None: 

-

89 raise EnvironmentVariableError(value) 

-

90 elif base == "steps": 

-

91 try: 

-

92 result = eval("self." + value) 

-

93 except AttributeError as e: 

-

94 raise EvaluationError(e) 

-

95 else: 

-

96 raise BaseContextError(base) 

-

97 

-

98 # Only replace the first occurrence 

-

99 expression = expression.replace(match, str(result), 1) 

-

100 

-

101 return expression 

-
- - - diff --git a/coverage/d_8e5d7c2a8ace5693_errors_py.html b/coverage/d_8e5d7c2a8ace5693_errors_py.html deleted file mode 100644 index 1a72031..0000000 --- a/coverage/d_8e5d7c2a8ace5693_errors_py.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - Coverage for api_validator/models/errors.py: 85% - - - - - -
-
-

- Coverage for api_validator/models/errors.py: - 85% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 13 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""errors.py: 

-

3 

-

4This module contains custom errors related to the models subpackage. 

-

5These errors mostly include exceptions related to resolving previous step and environment variable references in step files. 

-

6""" 

-

7 

-

8 

-

9class BaseContextError(Exception): 

-

10 """ 

-

11 Raises when the base context of an expression is not either "env" or "steps". 

-

12 """ 

-

13 def __init__(self, value): 

-

14 super().__init__( 

-

15 f"the base context must be either 'env' or 'steps', but found '{value}'" 

-

16 ) 

-

17 

-

18 

-

19class EvaluationError(Exception): 

-

20 """ 

-

21 Raises when the errors occur during expression evaluation. 

-

22 """ 

-

23 def __init__(self, message): 

-

24 super().__init__(message) 

-

25 

-

26 

-

27class EnvironmentVariableError(Exception): 

-

28 """ 

-

29 Raises when an environment variable is not found. 

-

30 """ 

-

31 def __init__(self, value): 

-

32 super().__init__(f"environment variable {value} not found") 

-

33 

-

34 

-

35class InvalidExpressionError(Exception): 

-

36 """ 

-

37 Raises when an expression is invalid. 

-

38 """ 

-

39 def __init__(self, value): 

-

40 super().__init__(f"invalid expression: {value}") 

-
- - - diff --git a/coverage/d_8e5d7c2a8ace5693_request_py.html b/coverage/d_8e5d7c2a8ace5693_request_py.html deleted file mode 100644 index ff472b6..0000000 --- a/coverage/d_8e5d7c2a8ace5693_request_py.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - Coverage for api_validator/models/request.py: 100% - - - - - -
-
-

- Coverage for api_validator/models/request.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 41 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""request.py 

-

3 

-

4Request will be sent off to a server to request or query some resource. 

-

5""" 

-

6 

-

7from addict import Dict 

-

8 

-

9from api_validator.models import Context 

-

10 

-

11context = Context() 

-

12 

-

13 

-

14class Request: 

-

15 """ 

-

16 Request object includes method, url, params, body, headers, and auth data. 

-

17 """ 

-

18 def __init__( 

-

19 self, 

-

20 method: str, 

-

21 url: str, 

-

22 params: dict = None, 

-

23 body: dict = None, 

-

24 headers: dict = None, 

-

25 auth: dict = None, 

-

26 ): 

-

27 self.method = method 

-

28 self.url = url 

-

29 self.params = params 

-

30 self.body = body 

-

31 self.headers = headers 

-

32 self.auth = auth 

-

33 

-

34 @property 

-

35 def params(self): 

-

36 return self._params 

-

37 

-

38 @params.setter 

-

39 def params(self, value): 

-

40 self._params = Dict(value) 

-

41 

-

42 @property 

-

43 def body(self): 

-

44 return self._body 

-

45 

-

46 @body.setter 

-

47 def body(self, value): 

-

48 self._body = Dict(value) 

-

49 

-

50 @property 

-

51 def headers(self): 

-

52 return self._headers 

-

53 

-

54 @headers.setter 

-

55 def headers(self, value): 

-

56 self._headers = Dict(value) 

-

57 

-

58 @property 

-

59 def auth(self): 

-

60 return self._auth 

-

61 

-

62 @auth.setter 

-

63 def auth(self, value): 

-

64 self._auth = Dict(value if value else {"username": "", "password": ""}) 

-

65 

-

66 def evaluate_all(self) -> None: 

-

67 """ 

-

68 Evaluates url, params, body, and header in-place. 

-

69 

-

70 Expressions can be either simple or nested: 

-

71 

-

72 /users/${{ steps.createUser.response.body.id }} 

-

73 /users/${{ steps.createUser.response.body.id }}/orders/${{ steps.getOrders.response.body[0].id }} 

-

74 """ 

-

75 

-

76 self.url = context.evaluate(self.url) 

-

77 

-

78 # Evaluate all Dict object 

-

79 for name in ("params", "body", "headers", "auth"): 

-

80 attr = self.__getattribute__(name) 

-

81 self.__setattr__(name, context.evaluate(attr)) 

-
- - - diff --git a/coverage/d_8e5d7c2a8ace5693_response_py.html b/coverage/d_8e5d7c2a8ace5693_response_py.html deleted file mode 100644 index 54faaf4..0000000 --- a/coverage/d_8e5d7c2a8ace5693_response_py.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - Coverage for api_validator/models/response.py: 100% - - - - - -
-
-

- Coverage for api_validator/models/response.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 12 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""response.py 

-

3 

-

4Response is generated once a request gets a response back from the server. 

-

5""" 

-

6 

-

7from dataclasses import dataclass 

-

8 

-

9import requests 

-

10from addict import Dict 

-

11 

-

12 

-

13@dataclass 

-

14class Response: 

-

15 """ 

-

16 Stores the response sent back to the server. 

-

17 """ 

-

18 

-

19 __response: requests.Response 

-

20 

-

21 def __getattr__(self, item): 

-

22 if item == "body": 

-

23 body = self.__response.json() 

-

24 return Dict(body) if isinstance(body, dict) else body 

-

25 

-

26 return getattr(self.__response, item) 

-
- - - diff --git a/coverage/d_8e5d7c2a8ace5693_schema_py.html b/coverage/d_8e5d7c2a8ace5693_schema_py.html deleted file mode 100644 index 5918a4c..0000000 --- a/coverage/d_8e5d7c2a8ace5693_schema_py.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - Coverage for api_validator/models/schema.py: 100% - - - - - -
-
-

- Coverage for api_validator/models/schema.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 10 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""schema.py 

-

3 

-

4JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. 

-

5""" 

-

6 

-

7from jschon import JSONSchema, JSON 

-

8from jschon.jsonschema import Scope 

-

9 

-

10META_SCHEMA = "https://json-schema.org/draft/2020-12/schema" 

-

11 

-

12 

-

13class Schema: 

-

14 """ 

-

15 Predefines meta schema and stores JSON schema document model. 

-

16 """ 

-

17 

-

18 def __init__(self, schema: dict, meta_schema: str = META_SCHEMA): 

-

19 schema["$schema"] = meta_schema 

-

20 self.schema = JSONSchema(schema) 

-

21 

-

22 def evaluate(self, json: dict) -> Scope: 

-

23 """ 

-

24 Verify the JSON against the schema. 

-

25 

-

26 Args: 

-

27 json (dict): the JSON document to evaluate 

-

28 

-

29 Returns: 

-

30 Evaluate a JSON document and return the complete evaluation result tree. 

-

31 """ 

-

32 

-

33 return self.schema.evaluate(JSON(json)) 

-
- - - diff --git a/coverage/d_8e5d7c2a8ace5693_singleton_py.html b/coverage/d_8e5d7c2a8ace5693_singleton_py.html deleted file mode 100644 index 0353604..0000000 --- a/coverage/d_8e5d7c2a8ace5693_singleton_py.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - Coverage for api_validator/models/singleton.py: 100% - - - - - -
-
-

- Coverage for api_validator/models/singleton.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 7 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""singleton.py 

-

3 

-

4This module is only used by Context to ensure the uniqueness of the global context state. 

-

5""" 

-

6 

-

7class Singleton(type): 

-

8 """ 

-

9 Restricts the instantiation of a class to one single instance. 

-

10 """ 

-

11 _instances = {} 

-

12 

-

13 def __call__(cls, *args, **kwargs): 

-

14 if cls not in cls._instances: 

-

15 cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 

-

16 return cls._instances[cls] 

-
- - - diff --git a/coverage/d_8e5d7c2a8ace5693_step_py.html b/coverage/d_8e5d7c2a8ace5693_step_py.html deleted file mode 100644 index 3e731af..0000000 --- a/coverage/d_8e5d7c2a8ace5693_step_py.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - Coverage for api_validator/models/step.py: 100% - - - - - -
-
-

- Coverage for api_validator/models/step.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 14 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""step.py: 

-

3 

-

4Encapsulates step data from the step file, including name, request, response, and schema for easy access. 

-

5""" 

-

6 

-

7from dataclasses import dataclass 

-

8 

-

9from jschon.jsonschema import Scope 

-

10 

-

11from .request import Request 

-

12from .response import Response 

-

13from .schema import Schema 

-

14 

-

15 

-

16@dataclass 

-

17class Step: 

-

18 """ 

-

19 Steps object manages request, response, and schema. 

-

20 """ 

-

21 

-

22 name: str 

-

23 request: Request = None 

-

24 response: Response = None 

-

25 schema: Schema = None 

-

26 

-

27 def verify(self) -> Scope: 

-

28 """ 

-

29 Verify the response against the schema. 

-

30 

-

31 Returns: 

-

32 Evaluate the response body and return the complete evaluation result tree. 

-

33 """ 

-

34 

-

35 return self.schema.evaluate(self.response.json()) 

-
- - - diff --git a/coverage/d_b4251882f9eb8989_action_py.html b/coverage/d_b4251882f9eb8989_action_py.html deleted file mode 100644 index cb68f07..0000000 --- a/coverage/d_b4251882f9eb8989_action_py.html +++ /dev/null @@ -1,315 +0,0 @@ - - - - - - Coverage for api_validator/action.py: 100% - - - - - -
-
-

- Coverage for api_validator/action.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 84 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""action.py 

-

3 

-

4This module contains the primary code for the GitHub Action. 

-

5Using the parsed OpenAPI specs and step files, it will create API requests and 

-

6validate the responses. 

-

7""" 

-

8import os 

-

9import traceback 

-

10 

-

11import requests 

-

12from actions_toolkit import core 

-

13from dotenv import load_dotenv 

-

14from jschon import create_catalog 

-

15from jschon.jsonschema import OutputFormat 

-

16from openapi_schema_to_json_schema import to_json_schema 

-

17from requests.auth import HTTPBasicAuth 

-

18 

-

19from .errors import ( 

-

20 InsufficientCoverageError, 

-

21 ResponseMatchError, 

-

22 ResponseValidationError, 

-

23 UndocumentedEndpointError, 

-

24) 

-

25from .models import Context, Request, Response, Schema, Step 

-

26from .parsing import parse_spec, parse_steps 

-

27from .preprocessing import get_endpoint_coverage 

-

28 

-

29# Global variable storing all runtime contexts (initialize once) 

-

30context = Context() 

-

31 

-

32# Load dotenv 

-

33load_dotenv() 

-

34 

-

35 

-

36def parse_spec_steps(spec_file_path: str, step_file_path: str) -> tuple[dict, dict]: 

-

37 """ 

-

38 Parses the OpenAPI spec and step files and return each of them 

-

39 

-

40 Args: 

-

41 spec_file_path (str): The path to the OpenAPI spec file 

-

42 step_file_path (str): The path to the step file 

-

43 Returns: 

-

44 A 2-tuple of dictionaries, containing the parsed OpenAPI spec and step files 

-

45 Raises: 

-

46 FileNotFoundError: If the spec or step file does not exist 

-

47 ValidationError: If the spec file is not valid according to the OpenAPI standard 

-

48 ScannerError: If the step file is not valid YAML 

-

49 JSONValidatorError: If the step file instance is not valid according to the JSON schema 

-

50 """ 

-

51 

-

52 # Parse OpenAPI specification file 

-

53 core.start_group("Parsing spec file") 

-

54 spec_data = parse_spec(spec_file_path) 

-

55 core.end_group() 

-

56 

-

57 # Parse step file 

-

58 core.start_group("Parsing step file") 

-

59 steps_data = parse_steps(step_file_path) 

-

60 core.end_group() 

-

61 

-

62 return spec_data, steps_data 

-

63 

-

64 

-

65def check_endpoint_coverage(spec_data: dict, steps_data: dict): 

-

66 """ 

-

67 Checks the endpoint coverage of the step file against the OpenAPI spec. 

-

68 

-

69 Args: 

-

70 spec_data (dict): The parsed OpenAPI spec 

-

71 steps_data (dict): The parsed step file 

-

72 Raises: 

-

73 UndocumentedEndpointError: 

-

74 """ 

-

75 

-

76 endpoint_coverage = get_endpoint_coverage(spec_data, steps_data) 

-

77 

-

78 # If any undocumented endpoints, immediately halt 

-

79 if endpoint_coverage.has_undocumented_endpoints(): 

-

80 raise UndocumentedEndpointError(endpoint_coverage.undocumented) 

-

81 

-

82 # Check if endpoint coverage meets threshold 

-

83 if "coverage_threshold" in steps_data: 

-

84 target_coverage: float = steps_data["coverage_threshold"] 

-

85 achieved_coverage = endpoint_coverage.proportion_covered() 

-

86 

-

87 if achieved_coverage < target_coverage: 

-

88 raise InsufficientCoverageError( 

-

89 achieved_coverage, target_coverage, endpoint_coverage.uncovered 

-

90 ) 

-

91 

-

92 

-

93def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: bool): 

-

94 """ 

-

95 Given a parsed OpenAPI spec and step file, make requests and validate responses. 

-

96 

-

97 Args: 

-

98 spec_data (dict): The parsed OpenAPI spec 

-

99 steps_data (dict): The parsed step file 

-

100 fail_fast (bool): Whether to exit immediately if exception raised 

-

101 verbose (bool): Whether to output stacktrace 

-

102 Raises: 

-

103 ResponseMatchError: If the response does not match the expected response according to status code 

-

104 ResponseValidationError: If the response does not match the expected response according to schema 

-

105 """ 

-

106 

-

107 core.info('Validating APIs') 

-

108 

-

109 # Create requests 

-

110 base_url: str = steps_data["base_url"] 

-

111 paths: dict = steps_data["paths"] 

-

112 

-

113 # Go through each path in the steps 

-

114 path_value: dict 

-

115 for path_key, path_value in paths.items(): 

-

116 core.start_group(path_key) 

-

117 

-

118 # Go through each method in each path 

-

119 method_value: dict 

-

120 for method_name, method_value in path_value.items(): 

-

121 # Store steps of current method 

-

122 context.clear_steps() 

-

123 

-

124 # Get steps YAML from file 

-

125 method_steps: list = method_value["steps"] 

-

126 

-

127 # Go through each step in each method 

-

128 step_data: dict 

-

129 for step_data in method_steps: 

-

130 try: 

-

131 # Get step name 

-

132 step_name = step_data.pop("name") 

-

133 core.info(step_name) 

-

134 

-

135 # Create Request object 

-

136 path_url = step_data.pop("url") 

-

137 request = Request(url=(base_url + path_url), **step_data) 

-

138 

-

139 # Evaluate expressions 

-

140 request.evaluate_all() 

-

141 

-

142 core.info(' Request:') 

-

143 core.info(f' {request.method} {request.url}') 

-

144 core.info(f' Authentication: {request.auth}') 

-

145 core.info(f' Body: {request.body}') 

-

146 core.info(f' Headers: {request.headers}') 

-

147 core.info(f' Parameters: {request.params}') 

-

148 

-

149 # Create Step object 

-

150 step = Step(step_name, request) 

-

151 

-

152 # Send the request 

-

153 step.response = Response( 

-

154 requests.request( 

-

155 method=request.method, 

-

156 url=request.url, 

-

157 params=request.params.to_dict(), 

-

158 headers=request.headers.to_dict(), 

-

159 json=request.body.to_dict(), 

-

160 auth=HTTPBasicAuth(**request.auth), 

-

161 ) 

-

162 ) 

-

163 

-

164 response = step.response 

-

165 

-

166 core.info(' Response:') 

-

167 core.info(f' HTTP {response.status_code} {response.reason}') 

-

168 core.info(f' Body: {response.body}') 

-

169 core.info('') 

-

170 

-

171 status_code = step.response.status_code 

-

172 

-

173 # Fetch schema 

-

174 try: 

-

175 schema = to_json_schema( 

-

176 spec_data.get("paths") 

-

177 .get(path_key) 

-

178 .get(method_name) 

-

179 .get("responses") 

-

180 .get(str(status_code)) 

-

181 .get("content") 

-

182 .get("application/json") 

-

183 .get("schema") 

-

184 ) 

-

185 step.schema = Schema(schema) 

-

186 except AttributeError: 

-

187 raise ResponseMatchError( 

-

188 spec_data.get("paths") 

-

189 .get(path_key) 

-

190 .get(method_name) 

-

191 .get("responses") 

-

192 .keys(), 

-

193 step.response, 

-

194 ) 

-

195 

-

196 # Save the step to further use 

-

197 context.add_steps(step) 

-

198 

-

199 # Verify the response 

-

200 verification_result = step.verify() 

-

201 if not verification_result.valid: 

-

202 raise ResponseValidationError( 

-

203 errors=verification_result.output(OutputFormat.BASIC)["errors"], 

-

204 url=path_url, 

-

205 method=method_name, 

-

206 status_code=status_code, 

-

207 ) 

-

208 

-

209 except BaseException as e: 

-

210 if fail_fast: 

-

211 raise e 

-

212 

-

213 if verbose: 

-

214 core.warning(traceback.format_exc(), title=e.__class__.__name__) 

-

215 else: 

-

216 core.warning(str(e), title=e.__class__.__name__) 

-

217 

-

218 core.end_group() 

-

219 

-

220 

-

221def verify_api(spec_file_path: str, step_file_path: str, fail_fast: bool = False, verbose: bool = False): 

-

222 """ 

-

223 This is the main function of the API verifier. 

-

224 It parses the OpenAPI spec and step files, measures coverage, makes requests, and validates responses. 

-

225 If this method completes without raising any exceptions, the API is considered valid. 

-

226 

-

227 Args: 

-

228 spec_file_path (str): The path to the OpenAPI spec file 

-

229 step_file_path (str): The path to the step file 

-

230 fail_fast (bool): Whether to exit immediately if exception raised 

-

231 verbose (bool): Whether to output stacktrace 

-

232 """ 

-

233 

-

234 create_catalog("2020-12", default=True) 

-

235 

-

236 # Parse spec and step files 

-

237 spec_data, steps_data = parse_spec_steps(spec_file_path, step_file_path) 

-

238 

-

239 # Check endpoint coverage 

-

240 check_endpoint_coverage(spec_data, steps_data) 

-

241 

-

242 # Make requests 

-

243 make_requests(spec_data, steps_data, fail_fast, verbose) 

-
- - - diff --git a/coverage/d_b4251882f9eb8989_errors_py.html b/coverage/d_b4251882f9eb8989_errors_py.html deleted file mode 100644 index d84654c..0000000 --- a/coverage/d_b4251882f9eb8989_errors_py.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - Coverage for api_validator/errors.py: 100% - - - - - -
-
-

- Coverage for api_validator/errors.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 18 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""errors.py 

-

3 

-

4This module contains custom errors for the API validator. 

-

5Often, these errors are raised when a library gives an error with a difficult to read message. 

-

6These errors will nicely format these error messages for logging and stack traces. 

-

7""" 

-

8 

-

9# Import errors from models subpackage 

-

10from .models.errors import * 

-

11 

-

12# For type hints 

-

13from .models import Response 

-

14 

-

15 

-

16class UndocumentedEndpointError(BaseException): 

-

17 """ 

-

18 Raised when the step file contains endpoints that are not documented in the OpenAPI spec. 

-

19 This error will halt further execution of the action. 

-

20 """ 

-

21 

-

22 def __init__(self, undocumented: set[str]): 

-

23 super().__init__( 

-

24 f"""The following endpoints are undocumented: 

-

25 {undocumented}""" 

-

26 ) 

-

27 

-

28 

-

29class InsufficientCoverageError(BaseException): 

-

30 """ 

-

31 Raised the step file does not sufficiently cover the OpenAPI spec according the the coverage threshold. 

-

32 If a coverage threshold given in the step file, this error will halt further execution of the action. 

-

33 """ 

-

34 

-

35 def __init__( 

-

36 self, achieved_coverage: float, target_coverage: float, uncovered: set[str] 

-

37 ): 

-

38 super().__init__( 

-

39 f"""The endpoint coverage is {achieved_coverage} but the target is {target_coverage}. 

-

40 The following endpoints are uncovered: {uncovered}""" 

-

41 ) 

-

42 

-

43 

-

44class ResponseMatchError(BaseException): 

-

45 """ 

-

46 Raised when a response does not match any of the expected responses in the OpenAPI spec according to status code. 

-

47 This error will halt further execution of the action. 

-

48 """ 

-

49 

-

50 def __init__(self, statuses: list, step: Response): 

-

51 super().__init__( 

-

52 f"""Cannot find a matching response in the specification. 

-

53 Expected possible responses: 

-

54 {', '.join(list(statuses))} 

-

55 Actual response: 

-

56 {step.status_code} {step.reason}: {step.text}""" 

-

57 ) 

-

58 

-

59 

-

60class ResponseValidationError(BaseException): 

-

61 """ 

-

62 Raised when the structure of a response does not match the expected schema in the OpenAPI spec. 

-

63 This error will halt further execution of the action. 

-

64 """ 

-

65 

-

66 def __init__(self, errors: list, url: str, method: str, status_code: int): 

-

67 super().__init__( 

-

68 f"""Response is not valid against the schema at: 

-

69 {url}: 

-

70 {method}: 

-

71 {status_code}: 

-

72 {list(map(lambda x: {x['keywordLocation']: x['error']}, errors))}""" 

-

73 ) 

-

74 

-

75 

-

76class JSONValidatorError(BaseException): 

-

77 """ 

-

78 Raised when the structure of a JSON does not match the expected JSON schema. 

-

79 This error will halt further execution of the action. 

-

80 """ 

-

81 

-

82 def __init__(self, errors: list[dict[str, str]]): 

-

83 super().__init__( 

-

84 "\n".join( 

-

85 [ 

-

86 f"""{error['instanceLocation']}\n {error['error']}""" 

-

87 for error in errors 

-

88 ] 

-

89 ) 

-

90 ) 

-
- - - diff --git a/coverage/d_b4251882f9eb8989_parsing_py.html b/coverage/d_b4251882f9eb8989_parsing_py.html deleted file mode 100644 index 93425ca..0000000 --- a/coverage/d_b4251882f9eb8989_parsing_py.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - Coverage for api_validator/parsing.py: 96% - - - - - -
-
-

- Coverage for api_validator/parsing.py: - 96% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 47 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""parsing.py 

-

3 

-

4This module contains functions for parsing the input file. 

-

5It will parse input files, returning a nested dictionary object. 

-

6If the input file is not valid, it will raise an exception. 

-

7""" 

-

8 

-

9import importlib.resources 

-

10import json 

-

11import os 

-

12 

-

13import yaml 

-

14from jschon import JSON, JSONSchema 

-

15from jschon.jsonschema import OutputFormat 

-

16from openapi_spec_validator import validate_spec 

-

17from prance import ResolvingParser, ValidationError 

-

18from prance.util.url import ResolutionError 

-

19from ruamel.yaml.scanner import ScannerError as RuamelScannerError 

-

20from yaml.constructor import SafeConstructor 

-

21from yaml.scanner import ScannerError as PyYAMLScannerError 

-

22 

-

23from .errors import JSONValidatorError 

-

24 

-

25 

-

26def parse_spec(spec_file_path: str) -> dict: 

-

27 """Parse OpenAPI spec file to a dictionary. 

-

28 

-

29 Args: 

-

30 spec_file_path (str): Path to spec file. 

-

31 

-

32 Returns: 

-

33 dict: Spec file parsed into a dictionary. 

-

34 

-

35 Raises: 

-

36 ValidationError: If the spec file is not valid according to the OpenAPI spec. 

-

37 ResolutionError: If the spec file has refs cannot be resolved 

-

38 ruamel.yaml.scanner.ScannerError: 

-

39 """ 

-

40 

-

41 if not os.path.exists(spec_file_path): 

-

42 raise FileNotFoundError(f"Spec file '{spec_file_path}' not found") 

-

43 

-

44 try: 

-

45 parser = ResolvingParser(spec_file_path) 

-

46 spec_dict = parser.specification 

-

47 validate_spec(spec_dict) 

-

48 return spec_dict 

-

49 except ValidationError as e: 

-

50 raise ValidationError( 

-

51 f"Spec file {spec_file_path} is not valid according to the OpenAPI specification" 

-

52 ) from e 

-

53 except ResolutionError as e: 

-

54 raise ResolutionError( 

-

55 f"Spec file {spec_file_path} has refs that cannot be resolved" 

-

56 ) 

-

57 except RuamelScannerError as e: 

-

58 raise RuamelScannerError( 

-

59 f"Could not parse {spec_file_path} as a YAML file" 

-

60 ) from e 

-

61 

-

62 

-

63def parse_steps(step_file_path: str) -> dict: 

-

64 """Parse steps file to a dictionary. 

-

65 

-

66 Args: 

-

67 step_file_path (str): Path to step file. 

-

68 

-

69 Returns: 

-

70 dict: Step file parsed into a dictionary. 

-

71 

-

72 Raises: 

-

73 FileNotFoundError: If the step file does not exist. 

-

74 yaml.scanner.ScannerError: If the step file is not valid YAML. 

-

75 JSONValidatorError: If the step file is not valid against the step file schema. 

-

76 """ 

-

77 

-

78 try: 

-

79 with importlib.resources.path( 

-

80 __package__, "steps_schema.json" 

-

81 ) as step_schema_file: 

-

82 steps_schema = JSONSchema(json.loads(step_schema_file.read_text())) 

-

83 except FileNotFoundError as e: 

-

84 raise FileNotFoundError(f"Steps schema file not found") from e 

-

85 

-

86 try: 

-

87 with open(step_file_path, "r") as step_file: 

-

88 # Drop support for datetime 

-

89 SafeConstructor.yaml_constructors[ 

-

90 "tag:yaml.org,2002:timestamp" 

-

91 ] = SafeConstructor.yaml_constructors["tag:yaml.org,2002:str"] 

-

92 

-

93 # Load the step file and validate it against the step file schema 

-

94 yaml_dict = yaml.safe_load(step_file) 

-

95 result = steps_schema.evaluate(JSON(yaml_dict)) 

-

96 result_output = result.output(OutputFormat.BASIC) 

-

97 

-

98 if not result_output["valid"]: 

-

99 raise JSONValidatorError(result_output["errors"]) 

-

100 except FileNotFoundError as e: 

-

101 raise FileNotFoundError(f"Steps file {step_file_path} not found") from e 

-

102 except PyYAMLScannerError as e: 

-

103 raise PyYAMLScannerError("Steps file is not valid YAML") from e 

-

104 

-

105 return yaml_dict 

-
- - - diff --git a/coverage/d_b4251882f9eb8989_preprocessing_py.html b/coverage/d_b4251882f9eb8989_preprocessing_py.html deleted file mode 100644 index 3b02a97..0000000 --- a/coverage/d_b4251882f9eb8989_preprocessing_py.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - Coverage for api_validator/preprocessing.py: 100% - - - - - -
-
-

- Coverage for api_validator/preprocessing.py: - 100% -

-
- - -
-

Shortcuts on this page

-
-

- r - m - x - p -   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
-
-

- 15 statements   - - - - -

-
- - - - -
-
-
-
-

1# -*- coding: utf-8 -*- 

-

2"""preprocessing.py 

-

3 

-

4This module contains functions to ensure compatibility for a step file and corresponding spec file. 

-

5It will check that every method referenced in the step file exists in the spec file. 

-

6""" 

-

7 

-

8from dataclasses import dataclass 

-

9 

-

10 

-

11@dataclass 

-

12class EndpointCoverage: 

-

13 """ 

-

14 Dictionary classifying endpoints into three categories: 

-

15 - covered: endpoints that are called in the step file and documented in the spec file 

-

16 - uncovered: endpoints that are in the spec file but not called in the step file 

-

17 - undocumented: endpoints that are called in the step file but not documented in the spec file 

-

18 """ 

-

19 

-

20 covered: set[str] 

-

21 uncovered: set[str] 

-

22 undocumented: set[str] 

-

23 

-

24 def has_undocumented_endpoints(self) -> bool: 

-

25 return len(self.undocumented) > 0 

-

26 

-

27 def proportion_covered(self) -> float: 

-

28 return len(self.covered) / ( 

-

29 len(self.covered) + len(self.uncovered) + len(self.undocumented) 

-

30 ) 

-

31 

-

32 

-

33def get_endpoint_coverage(spec: dict, step: dict) -> EndpointCoverage: 

-

34 """ 

-

35 Given a parsed spec and step file, determine the endpoints in the spec that are achieved by the step file. 

-

36 

-

37 Args: 

-

38 spec (dict): specification file parsed as dict 

-

39 step (dict): step file parsed as dict 

-

40 

-

41 Returns: 

-

42 EndpointCoverage: A dataclass containing the endpoints covered, uncovered, and undocumented 

-

43 """ 

-

44 

-

45 # Get all endpoints in the spec file 

-

46 spec_endpoints = set(spec["paths"].keys()) 

-

47 

-

48 # Get all endpoints in the step file 

-

49 step_endpoints = set(step["paths"].keys()) 

-

50 

-

51 return EndpointCoverage( 

-

52 covered=spec_endpoints & step_endpoints, 

-

53 uncovered=spec_endpoints - step_endpoints, 

-

54 undocumented=step_endpoints - spec_endpoints, 

-

55 ) 

-
- - - diff --git a/coverage/favicon_32.png b/coverage/favicon_32.png deleted file mode 100644 index 8649f0475d8d20793b2ec431fe25a186a414cf10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1732 zcmV;#20QtQP)K2KOkBOVxIZChq#W-v7@TU%U6P(wycKT1hUJUToW3ke1U1ONa4 z000000000000000bb)GRa9mqwR9|UWHy;^RUrt?IT__Y0JUcxmBP0(51q1>E00030 z|NrOz)aw7%8sJzM<5^g%z7^qE`}_Ot|JUUG(NUkWzR|7K?Zo%@_v-8G-1N%N=D$;; zw;keH4dGY$`1t4M=HK_s*zm^0#KgqfwWhe3qO_HtvXYvtjgX>;-~C$L`&k>^R)9)7 zdPh2TL^pCnHC#0+_4D)M`p?qp!pq{jO_{8;$fbaflbx`Tn52n|n}8VFRTA1&ugOP< zPd{uvFjz7t*Vot1&d$l-xWCk}s;sQL&#O(Bskh6gqNJv>#iB=ypG1e3K!K4yc7!~M zfj4S*g^zZ7eP$+_Sl07Z646l;%urinP#D8a6TwRtnLIRcI!r4f@bK~9-`~;E(N?Lv zSEst7s;rcxsi~}{Nsytfz@MtUoR*iFc8!#vvx}Umhm4blk(_~MdVD-@dW&>!Nn~ro z_E~-ESVQAj6Wmn;(olz(O&_{U2*pZBc1aYjMh>Dq3z|6`jW`RDHV=t3I6yRKJ~LOX zz_z!!vbVXPqob#=pj3^VMT?x6t(irRmSKsMo1~LLkB&=#j!=M%NP35mfqim$drWb9 zYIb>no_LUwc!r^NkDzs4YHu@=ZHRzrafWDZd1EhEVq=tGX?tK$pIa)DTh#bkvh!J- z?^%@YS!U*0E8$q$_*aOTQ&)Ra64g>ep;BdcQgvlg8qQHrP*E$;P{-m=A*@axn@$bO zO-Y4JzS&EAi%YG}N?cn?YFS7ivPY=EMV6~YH;+Xxu|tefLS|Aza)Cg6us#)=JW!uH zQa?H>d^j+YHCtyjL^LulF*05|F$RG!AX_OHVI&MtA~_@=5_lU|0000rbW%=J06GH4 z^5LD8b8apw8vNh1ua1mF{{Hy)_U`NA;Nacc+sCpuHXa-V{r&yz?c(9#+}oX+NmiRW z+W-IqK1oDDR5;6GfCDCOP5}iL5fK(cB~ET81`MFgF2kGa9AjhSIk~-E-4&*tPPKdiilQJ11k_J082ZS z>@TvivP!5ZFG?t@{t+GpR3XR&@*hA_VE1|Lo8@L@)l*h(Z@=?c-NS$Fk&&61IzUU9 z*nPqBM=OBZ-6ka1SJgGAS-Us5EN)r#dUX%>wQZLa2ytPCtMKp)Ob z*xcu38Z&d5<-NBS)@jRD+*!W*cf-m_wmxDEqBf?czI%3U0J$Xik;lA`jg}VH?(S(V zE!M3;X2B8w0TnnW&6(8;_Uc)WD;Ms6PKP+s(sFgO!}B!^ES~GDt4qLPxwYB)^7)XA zZwo9zDy-B0B+jT6V=!=bo(zs_8{eBA78gT9GH$(DVhz;4VAYwz+bOIdZ-PNb|I&rl z^XG=vFLF)1{&nT2*0vMz#}7^9hXzzf&ZdKlEj{LihP;|;Ywqn35ajP?H?7t|i-Un% z&&kxee@9B{nwgv1+S-~0)E1{ob1^Wn`F2isurqThKK=3%&;`@{0{!D- z&CSj80t;uPu&FaJFtSXKH#ajgGj}=sEad7US6jP0|Db@0j)?(5@sf<7`~a9>s;wCa zm^)spe{uxGFmrJYI9cOh7s$>8Npkt-5EWB1UKc`{W{y5Ce$1+nM9Cr;);=Ju#N^62OSlJMn7omiUgP&ErsYzT~iGxcW aE(`!K@+CXylaC4j0000 - - - - API Validator Test Coverage - - - - - -
-
-

API Validator Test Coverage: - 98% -

-
- - -
-

Shortcuts on this page

-
-

- n - s - m - x - b - p - c   change column sorting -

-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Modulestatementsmissingexcludedbranchespartialcoverage
api_validator/action.py8400180100%
api_validator/errors.py180040100%
api_validator/models/context.py423022194%
api_validator/models/errors.py13200085%
api_validator/models/request.py410020100%
api_validator/models/response.py120040100%
api_validator/models/schema.py100000100%
api_validator/models/singleton.py70020100%
api_validator/models/step.py140020100%
api_validator/parsing.py472010096%
api_validator/preprocessing.py150020100%
Total3037066198%
-

- No items found using the specified filter. -

-
- - - diff --git a/coverage/index.js b/coverage/index.js deleted file mode 100644 index e057505..0000000 --- a/coverage/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - - diff --git a/coverage/keybd_closed.png b/coverage/keybd_closed.png deleted file mode 100644 index ba119c47df81ed2bbd27a06988abf700139c4f99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9004 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 diff --git a/coverage/keybd_open.png b/coverage/keybd_open.png deleted file mode 100644 index a8bac6c9de256626c680f9e9e3f8ee81d9713ecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9003 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 G%6|bpD&^7u diff --git a/coverage/status.json b/coverage/status.json deleted file mode 100644 index 54bfe78..0000000 --- a/coverage/status.json +++ /dev/null @@ -1 +0,0 @@ -{"format":2,"version":"6.1.1","globals":"17b24595ac4974bc95d8570644d1e9fd","files":{"d_b4251882f9eb8989_action_py":{"hash":"e6512aa9e22b369a03837f18b2e0476d","index":{"nums":[0,1,84,0,0,18,0,0],"html_filename":"d_b4251882f9eb8989_action_py.html","relative_filename":"api_validator/action.py"}},"d_b4251882f9eb8989_errors_py":{"hash":"9c6522f1d26f4c7869aa35b7cdb355aa","index":{"nums":[0,1,18,0,0,4,0,0],"html_filename":"d_b4251882f9eb8989_errors_py.html","relative_filename":"api_validator/errors.py"}},"d_8e5d7c2a8ace5693_context_py":{"hash":"560a88d00f4364714e5bbca960490478","index":{"nums":[0,1,42,0,3,22,1,1],"html_filename":"d_8e5d7c2a8ace5693_context_py.html","relative_filename":"api_validator/models/context.py"}},"d_8e5d7c2a8ace5693_errors_py":{"hash":"2f6f826eb788afdd8fbe50c8ad953679","index":{"nums":[0,1,13,0,2,0,0,0],"html_filename":"d_8e5d7c2a8ace5693_errors_py.html","relative_filename":"api_validator/models/errors.py"}},"d_8e5d7c2a8ace5693_request_py":{"hash":"6a9bab1b43528141d458197ab206bce3","index":{"nums":[0,1,41,0,0,2,0,0],"html_filename":"d_8e5d7c2a8ace5693_request_py.html","relative_filename":"api_validator/models/request.py"}},"d_8e5d7c2a8ace5693_response_py":{"hash":"a03b666ae4bb9c0c0617b4caa50ab697","index":{"nums":[0,1,12,0,0,4,0,0],"html_filename":"d_8e5d7c2a8ace5693_response_py.html","relative_filename":"api_validator/models/response.py"}},"d_8e5d7c2a8ace5693_schema_py":{"hash":"ba30e1f6f6743c8514c5d0ece4905ae1","index":{"nums":[0,1,10,0,0,0,0,0],"html_filename":"d_8e5d7c2a8ace5693_schema_py.html","relative_filename":"api_validator/models/schema.py"}},"d_8e5d7c2a8ace5693_singleton_py":{"hash":"cb28363fc47ab8aaea9f183966c0faae","index":{"nums":[0,1,7,0,0,2,0,0],"html_filename":"d_8e5d7c2a8ace5693_singleton_py.html","relative_filename":"api_validator/models/singleton.py"}},"d_8e5d7c2a8ace5693_step_py":{"hash":"40d88d69c0b8c4756c3acbc5209134c4","index":{"nums":[0,1,14,0,0,2,0,0],"html_filename":"d_8e5d7c2a8ace5693_step_py.html","relative_filename":"api_validator/models/step.py"}},"d_b4251882f9eb8989_parsing_py":{"hash":"c6dbfd4219bdc0bf1c478330fb83fa5b","index":{"nums":[0,1,47,0,2,10,0,0],"html_filename":"d_b4251882f9eb8989_parsing_py.html","relative_filename":"api_validator/parsing.py"}},"d_b4251882f9eb8989_preprocessing_py":{"hash":"2f3a11d15bc1266941e441b4a5180838","index":{"nums":[0,1,15,0,0,2,0,0],"html_filename":"d_b4251882f9eb8989_preprocessing_py.html","relative_filename":"api_validator/preprocessing.py"}}}} \ No newline at end of file diff --git a/coverage/style.css b/coverage/style.css deleted file mode 100644 index cca6f11..0000000 --- a/coverage/style.css +++ /dev/null @@ -1,307 +0,0 @@ -@charset "UTF-8"; -/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ -/* Don't edit this .css file. Edit the .scss file instead! */ -html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } - -body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } - -@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { body { color: #eee; } } - -html > body { font-size: 16px; } - -a:active, a:focus { outline: 2px dashed #007acc; } - -p { font-size: .875em; line-height: 1.4em; } - -table { border-collapse: collapse; } - -td { vertical-align: top; } - -table tr.hidden { display: none !important; } - -p#no_rows { display: none; font-size: 1.2em; } - -a.nav { text-decoration: none; color: inherit; } - -a.nav:hover { text-decoration: underline; color: inherit; } - -header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } - -@media (prefers-color-scheme: dark) { header { background: black; } } - -@media (prefers-color-scheme: dark) { header { border-color: #333; } } - -header .content { padding: 1rem 3.5rem; } - -header h2 { margin-top: .5em; font-size: 1em; } - -header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } - -header.sticky .text { display: none; } - -header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } - -header.sticky .content { padding: 0.5rem 3.5rem; } - -header.sticky .content p { font-size: 1em; } - -header.sticky ~ #source { padding-top: 6.5em; } - -main { position: relative; z-index: 1; } - -.indexfile footer { margin: 1rem 3.5rem; } - -.pyfile footer { margin: 1rem 1rem; } - -footer .content { padding: 0; color: #666; font-style: italic; } - -@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } - -#index { margin: 1rem 0 0 3.5rem; } - -h1 { font-size: 1.25em; display: inline-block; } - -#filter_container { float: right; margin: 0 2em 0 0; } - -#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } - -@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } - -@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } - -#filter_container input:focus { border-color: #007acc; } - -header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } - -@media (prefers-color-scheme: dark) { header button { border-color: #444; } } - -header button:active, header button:focus { outline: 2px dashed #007acc; } - -header button.run { background: #eeffee; } - -@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } - -header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } - -header button.mis { background: #ffeeee; } - -@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } - -header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } - -header button.exc { background: #f7f7f7; } - -@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } - -header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } - -header button.par { background: #ffffd5; } - -@media (prefers-color-scheme: dark) { header button.par { background: #650; } } - -header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } - -@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } - -#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } - -#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } - -#help_panel_wrapper { float: right; position: relative; } - -#keyboard_icon { margin: 5px; } - -#help_panel_state { display: none; } - -#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; } - -#help_panel .legend { font-style: italic; margin-bottom: 1em; } - -.indexfile #help_panel { width: 25em; } - -.pyfile #help_panel { width: 18em; } - -#help_panel_state:checked ~ #help_panel { display: block; } - -.keyhelp { margin-top: .75em; } - -kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } - -#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } - -#source p { position: relative; white-space: pre; } - -#source p * { box-sizing: border-box; } - -#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } - -#source p .n.highlight { background: #ffdd00; } - -#source p .n a { margin-top: -4em; padding-top: 4em; text-decoration: none; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } - -#source p .n a:hover { text-decoration: underline; color: #999; } - -@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } - -#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } - -@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } - -#source p .t:hover { background: #f2f2f2; } - -@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } - -#source p .t:hover ~ .r .annotate.long { display: block; } - -#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } - -@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } - -#source p .t .key { font-weight: bold; line-height: 1px; } - -#source p .t .str { color: #0451a5; } - -@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } - -#source p.mis .t { border-left: 0.2em solid #ff0000; } - -#source p.mis.show_mis .t { background: #fdd; } - -@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } - -#source p.mis.show_mis .t:hover { background: #f2d2d2; } - -@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } - -#source p.run .t { border-left: 0.2em solid #00dd00; } - -#source p.run.show_run .t { background: #dfd; } - -@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } - -#source p.run.show_run .t:hover { background: #d2f2d2; } - -@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } - -#source p.exc .t { border-left: 0.2em solid #808080; } - -#source p.exc.show_exc .t { background: #eee; } - -@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } - -#source p.exc.show_exc .t:hover { background: #e2e2e2; } - -@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } - -#source p.par .t { border-left: 0.2em solid #bbbb00; } - -#source p.par.show_par .t { background: #ffa; } - -@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } - -#source p.par.show_par .t:hover { background: #f2f2a2; } - -@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } - -#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } - -#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } - -@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } - -#source p .annotate.short:hover ~ .long { display: block; } - -#source p .annotate.long { width: 30em; right: 2.5em; } - -#source p input { display: none; } - -#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } - -#source p input ~ .r label.ctx::before { content: "▶ "; } - -#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } - -@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } - -@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } - -#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } - -@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } - -@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } - -#source p input:checked ~ .r label.ctx::before { content: "▼ "; } - -#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } - -#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } - -@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } - -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; } - -@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } - -#source p .ctxs span { display: block; text-align: right; } - -#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } - -#index table.index { margin-left: -.5em; } - -#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } - -@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } - -#index td.name, #index th.name { text-align: left; width: auto; } - -#index th { font-style: italic; color: #333; cursor: pointer; } - -@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } - -#index th:hover { background: #eee; } - -@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } - -#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } - -@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } - -#index th[aria-sort="ascending"]::after { font-family: sans-serif; content: " ↑"; } - -#index th[aria-sort="descending"]::after { font-family: sans-serif; content: " ↓"; } - -#index td.name a { text-decoration: none; color: inherit; } - -#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } - -#index tr.file:hover { background: #eee; } - -@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } - -#index tr.file:hover td.name { text-decoration: underline; color: inherit; } - -#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } - -@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } - -@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } - -#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } - -@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/requirements.dev.txt b/requirements.dev.txt index 4582efc..e282d2d 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,9 +1,6 @@ -coverage==6.1.1 Django==3.2.9 django_extensions==3.1.3 djangorestframework==3.12.4 Faker==9.8.0 genbadge[coverage]==1.0.6 pytest==7.0.1 -Sphinx==4.2.0 -sphinx-rtd-theme==1.0.0 diff --git a/requirements.txt b/requirements.txt index a64ec26..7ce0a7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,3 @@ python-dotenv==0.19.1 PyYAML==6.0 requests==2.26.0 rich==11.2.0 -actions-toolkit==0.0.9 diff --git a/src/validate.py b/src/validate.py index 0012cda..da38233 100644 --- a/src/validate.py +++ b/src/validate.py @@ -9,7 +9,6 @@ import traceback import requests -from actions_toolkit import core from rich import print, print_json from dotenv import load_dotenv from jschon import create_catalog @@ -51,14 +50,12 @@ def parse_spec_steps(spec_file_path: str, step_file_path: str) -> tuple[dict, di """ # Parse OpenAPI specification file - core.start_group("Parsing spec file") + print("---Parsing spec file---") spec_data = parse_spec(spec_file_path) - core.end_group() # Parse step file - core.start_group("Parsing step file") + print("---Parsing step file---") steps_data = parse_steps(step_file_path) - core.end_group() return spec_data, steps_data @@ -105,7 +102,7 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b ResponseValidationError: If the response does not match the expected response according to schema """ - print('Validating APIs') + print('---Validating APIs---') # Create requests base_url: str = steps_data["base_url"] @@ -114,8 +111,6 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b # Go through each path in the steps path_value: dict for path_key, path_value in paths.items(): - core.start_group(path_key) - # Go through each method in each path method_value: dict for method_name, method_value in path_value.items(): @@ -140,13 +135,13 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b # Evaluate expressions request.evaluate_all() - print(' Request:') - print(f' {request.method} {request.url}') - print(f' Authentication: {request.auth}') - print(f' Body:') + print('Request:') + print(f'{request.method} {request.url}') + print(f'Authentication: {request.auth}') + print(f'Body:') print_json(data=request.body, indent=4) - print(f' Headers: {request.headers}') - print(f' Parameters: {request.params}') + print(f'Headers: {request.headers}') + print(f'Parameters: {request.params}') # Create Step object step = Step(step_name, request) @@ -165,8 +160,8 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b response = step.response - print(' Response:') - print(f' HTTP {response.status_code} {response.reason}') + print('Response:') + print(f'HTTP {response.status_code} {response.reason}') print_json(data=response.body, indent=4) print('') @@ -213,11 +208,9 @@ def make_requests(spec_data: dict, steps_data: dict, fail_fast: bool, verbose: b raise e if verbose: - core.warning(traceback.format_exc(), title=e.__class__.__name__) + print(f'[red]{traceback.format_exc()}[/red]') else: - core.warning(str(e), title=e.__class__.__name__) - - core.end_group() + print(f'[bold red]{str(e)}[/bold red]') def verify_api(spec_file_path: str, step_file_path: str, fail_fast: bool = False, verbose: bool = False): From 153a446916662a65fab69f4b3d51b66fe4c715f0 Mon Sep 17 00:00:00 2001 From: AJ Rice <53190766+ajrice6713@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:40:59 -0500 Subject: [PATCH 9/9] Fix Workflow Took 7 minutes --- .github/workflows/ci.yml | 6 ++---- README.md | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21d1109..5f7fb98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,5 @@ jobs: pip install --upgrade pip pip install -r requirements.txt pip install -r requirements.dev.txt - - name: Tests with coverage - run: | - coverage run --branch -m pytest -vv - coverage report -m + - name: Test + run: pytest diff --git a/README.md b/README.md index 210bc55..d655e60 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # PyPony -[![Unit Tests](https://github.com/Bandwidth/pypony/actions/workflows/ci.yml/badge.svg)](https://github.com/Bandwidth/pypony/actions/workflows/ci.yml) -[![coverage badge](coverage/badge.svg)](https://github.com/Bandwidth/pypony/coverage/) +[![Unit Tests](https://github.com/Bandwidth/pypony/actions/workflows/ci.yml/badge.svg)](https://github.com/Bandwidth/pypony/actions/workflows/ci.yml) PyPony is a Python CLI tool for contract testing OpenAPI specifications against the live APIs that they define.