From a78d136970d6e92a67b536f7afe86d599aa2bee6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:04:13 +0000 Subject: [PATCH 1/5] Initial plan From dc05c76b7a624b92cf9dd8a520391a3258da768f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:11:08 +0000 Subject: [PATCH 2/5] Add compatibility badge and skip Swagger 2.0 schemas in tests Co-authored-by: phalt <490685+phalt@users.noreply.github.com> --- .github/compatibility.json | 1 + .github/workflows/test-openapi-directory.yml | 41 ++++++++++- README.md | 1 + test_openapi_directory.py | 71 +++++++++++++++----- 4 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 .github/compatibility.json diff --git a/.github/compatibility.json b/.github/compatibility.json new file mode 100644 index 0000000..d7fc723 --- /dev/null +++ b/.github/compatibility.json @@ -0,0 +1 @@ +{"schemaVersion": 1, "label": "compatibility", "message": "0.00%", "color": "brightgreen"} diff --git a/.github/workflows/test-openapi-directory.yml b/.github/workflows/test-openapi-directory.yml index 9f80999..d79f1e3 100644 --- a/.github/workflows/test-openapi-directory.yml +++ b/.github/workflows/test-openapi-directory.yml @@ -10,7 +10,7 @@ jobs: test-openapi-directory: runs-on: ubuntu-latest permissions: - contents: read + contents: write # Need write permission to commit the badge data steps: #---------------------------------------------- # check-out repo and set-up python @@ -44,4 +44,41 @@ jobs: #---------------------------------------------- - name: Test against APIs-guru/openapi-directory run: | - uv run python3 test_openapi_directory.py --verbose + uv run python3 test_openapi_directory.py --verbose 2>&1 | tee test_output.txt + + #---------------------------------------------- + # Extract success rate and create badge data + #---------------------------------------------- + - name: Extract compatibility percentage + run: | + # Extract the percentage from the test output using sed for portability + PERCENTAGE=$(sed -n 's/.*Success rate: \([0-9]*\.[0-9]*\)%.*/\1/p' test_output.txt) + + # Check if extraction was successful + if [ -z "$PERCENTAGE" ]; then + echo "Error: Failed to extract percentage from test output" + echo "Falling back to previous value if exists, or using 0.00" + if [ -f .github/compatibility.json ]; then + echo "Keeping existing compatibility.json" + exit 0 + else + PERCENTAGE="0.00" + fi + fi + + # Create JSON file for shields.io endpoint badge + mkdir -p .github + echo "{\"schemaVersion\": 1, \"label\": \"compatibility\", \"message\": \"${PERCENTAGE}%\", \"color\": \"brightgreen\"}" > .github/compatibility.json + + echo "Extracted compatibility: ${PERCENTAGE}%" + + #---------------------------------------------- + # Commit and push the badge data + #---------------------------------------------- + - name: Commit compatibility data + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add .github/compatibility.json + git diff --staged --quiet || git commit -m "Update compatibility badge data: $(date -u '+%Y-%m-%d')" + git push diff --git a/README.md b/README.md index d666744..d9532b6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Package version](https://img.shields.io/pypi/v/cicerone?color=%2334D058&label=latest%20version)](https://pypi.org/project/cicerone) [![codecov](https://codecov.io/github/phalt/cicerone/graph/badge.svg?token=BAQE27Z4Y7)](https://codecov.io/github/phalt/cicerone) ![PyPI - License](https://img.shields.io/pypi/l/cicerone) +![OpenAPI Compatibility](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/phalt/cicerone/main/.github/compatibility.json) Cicerone parses OpenAPI schemas into Pydantic models for introspection and traversal. diff --git a/test_openapi_directory.py b/test_openapi_directory.py index 4c0f943..0e1f4f5 100644 --- a/test_openapi_directory.py +++ b/test_openapi_directory.py @@ -46,44 +46,65 @@ def find_schema_files(base_dir: pathlib.Path) -> List[pathlib.Path]: return sorted(schema_files) -def test_schema_file(schema_path: pathlib.Path) -> Tuple[bool, str, Exception | None]: +def test_schema_file(schema_path: pathlib.Path) -> Tuple[str, str, Exception | None]: """Test parsing a single schema file. Returns: - Tuple of (success: bool, error_message: str, exception: Exception | None) + Tuple of (status: str, error_message: str, exception: Exception | None) + where status is one of: "success", "skipped", "failed" """ try: spec = cicerone_parse.parse_spec_from_file(schema_path) # Basic validation - ensure we got a spec with some content if spec is None: - return False, "Parsed spec is None", None - return True, "", None + return "failed", "Parsed spec is None", None + + # Check if this is a Swagger 2.x file (even if cicerone auto-converts it) + # Cicerone preserves the original format in spec.raw + if "swagger" in spec.raw: + swagger_version = str(spec.raw["swagger"]) + # Check if it's Swagger 2.x (2.0, 2.1, etc.) + try: + if swagger_version.split(".")[0] == "2": + return "skipped", f"Swagger {swagger_version} (not supported, cicerone requires OpenAPI 3.x)", None + except (IndexError, ValueError): + pass # If we can't parse version, continue with normal processing + + return "success", "", None except Exception as e: - return False, f"{type(e).__name__}: {str(e)}", e + return "failed", f"{type(e).__name__}: {str(e)}", e def test_all_schemas( schema_files: List[pathlib.Path], base_dir: pathlib.Path, verbose: bool = False, fail_fast: bool = False -) -> Tuple[int, int, List[Tuple[pathlib.Path, str, Exception | None]]]: +) -> Tuple[int, int, int, List[Tuple[pathlib.Path, str, Exception | None]], List[Tuple[pathlib.Path, str]]]: """Test parsing all schema files. Returns: - Tuple of (success_count, failure_count, failures_list) + Tuple of (success_count, skipped_count, failure_count, failures_list, skipped_list) """ print(f"\nTesting {len(schema_files)} schemas...") successes = 0 + skipped = [] failures = [] for i, schema_path in enumerate(schema_files, 1): if verbose or i % 100 == 0: - print(f"Progress: {i}/{len(schema_files)} ({successes} successful, {len(failures)} failed)") + print( + f"Progress: {i}/{len(schema_files)} ({successes} successful, " + f"{len(skipped)} skipped, {len(failures)} failed)" + ) - success, error, exception = test_schema_file(schema_path) - if success: + status, error, exception = test_schema_file(schema_path) + if status == "success": successes += 1 if verbose: print(f" ✓ {schema_path.relative_to(base_dir)}") - else: + elif status == "skipped": + skipped.append((schema_path, error)) + if verbose: + print(f" ⊘ {schema_path.relative_to(base_dir)}: {error}") + else: # failed failures.append((schema_path, error, exception)) if verbose: print(f" ✗ {schema_path.relative_to(base_dir)}: {error}") @@ -102,7 +123,7 @@ def test_all_schemas( print(f"\nSchema location: {schema_path}") break - return successes, len(failures), failures + return successes, len(skipped), len(failures), failures, skipped def main() -> int: @@ -140,7 +161,7 @@ def main() -> int: schema_files = schema_files[: args.limit] # Test all schemas - successes, failures_count, failures = test_all_schemas( + successes, skipped_count, failures_count, failures, skipped = test_all_schemas( schema_files, repo_dir, verbose=args.verbose, fail_fast=args.fail_fast ) @@ -148,10 +169,30 @@ def main() -> int: print("\n" + "=" * 80) print("SUMMARY") print("=" * 80) - print(f"Total schemas tested: {len(schema_files)}") + print(f"Total schemas found: {len(schema_files)}") print(f"Successful: {successes}") + print(f"Skipped (version incompatible): {skipped_count}") print(f"Failed: {failures_count}") - print(f"Success rate: {successes / len(schema_files) * 100:.2f}%") + + # Calculate success rate excluding skipped schemas + testable_count = len(schema_files) - skipped_count + if testable_count > 0: + print( + f"Success rate: {successes / testable_count * 100:.2f}% ({successes}/{testable_count} testable schemas)" + ) + else: + print("Success rate: N/A (no testable schemas)") + + if skipped: + print(f"\n{len(skipped)} schemas skipped due to version incompatibility:") + for schema_path, reason in skipped[:5]: # Show first 5 skipped + rel_path = schema_path.relative_to(repo_dir) + print(f" - {rel_path}") + if args.verbose: + print(f" Reason: {reason}") + + if len(skipped) > 5: + print(f" ... and {len(skipped) - 5} more") if failures: print(f"\n{len(failures)} schemas failed to parse:") From dd963012c3e8c1ffd46acb40e22a9354dc27d83f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:12:45 +0000 Subject: [PATCH 3/5] Fix regex to handle whole number percentages and improve version handling Co-authored-by: phalt <490685+phalt@users.noreply.github.com> --- .github/workflows/test-openapi-directory.yml | 2 +- test_openapi_directory.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-openapi-directory.yml b/.github/workflows/test-openapi-directory.yml index d79f1e3..8929d56 100644 --- a/.github/workflows/test-openapi-directory.yml +++ b/.github/workflows/test-openapi-directory.yml @@ -52,7 +52,7 @@ jobs: - name: Extract compatibility percentage run: | # Extract the percentage from the test output using sed for portability - PERCENTAGE=$(sed -n 's/.*Success rate: \([0-9]*\.[0-9]*\)%.*/\1/p' test_output.txt) + PERCENTAGE=$(sed -n 's/.*Success rate: \([0-9]*\(\.[0-9]*\)\?\)%.*/\1/p' test_output.txt) # Check if extraction was successful if [ -z "$PERCENTAGE" ]; then diff --git a/test_openapi_directory.py b/test_openapi_directory.py index 0e1f4f5..e3b892e 100644 --- a/test_openapi_directory.py +++ b/test_openapi_directory.py @@ -62,11 +62,13 @@ def test_schema_file(schema_path: pathlib.Path) -> Tuple[str, str, Exception | N # Check if this is a Swagger 2.x file (even if cicerone auto-converts it) # Cicerone preserves the original format in spec.raw if "swagger" in spec.raw: - swagger_version = str(spec.raw["swagger"]) + swagger_version = spec.raw["swagger"] # Check if it's Swagger 2.x (2.0, 2.1, etc.) try: - if swagger_version.split(".")[0] == "2": - return "skipped", f"Swagger {swagger_version} (not supported, cicerone requires OpenAPI 3.x)", None + # Handle both string and numeric versions + version_str = str(swagger_version) + if version_str.split(".")[0] == "2": + return "skipped", f"Swagger {version_str} (not supported, cicerone requires OpenAPI 3.x)", None except (IndexError, ValueError): pass # If we can't parse version, continue with normal processing From 4c8729f63ede2428bc5274794ef3d9a92a754922 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 16:14:33 +0000 Subject: [PATCH 4/5] Improve error handling for edge cases in version detection and percentage output Co-authored-by: phalt <490685+phalt@users.noreply.github.com> --- test_openapi_directory.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test_openapi_directory.py b/test_openapi_directory.py index e3b892e..7c94a6d 100644 --- a/test_openapi_directory.py +++ b/test_openapi_directory.py @@ -67,7 +67,8 @@ def test_schema_file(schema_path: pathlib.Path) -> Tuple[str, str, Exception | N try: # Handle both string and numeric versions version_str = str(swagger_version) - if version_str.split(".")[0] == "2": + # Ensure version_str is not empty and has at least one part + if version_str and version_str.split(".")[0] == "2": return "skipped", f"Swagger {version_str} (not supported, cicerone requires OpenAPI 3.x)", None except (IndexError, ValueError): pass # If we can't parse version, continue with normal processing @@ -179,11 +180,13 @@ def main() -> int: # Calculate success rate excluding skipped schemas testable_count = len(schema_files) - skipped_count if testable_count > 0: + success_rate = successes / testable_count * 100 print( - f"Success rate: {successes / testable_count * 100:.2f}% ({successes}/{testable_count} testable schemas)" + f"Success rate: {success_rate:.2f}% ({successes}/{testable_count} testable schemas)" ) else: - print("Success rate: N/A (no testable schemas)") + # Output 0.00% for consistency with badge extraction + print("Success rate: 0.00% (no testable schemas)") if skipped: print(f"\n{len(skipped)} schemas skipped due to version incompatibility:") From 2ca775f1b447a5804a3929ce8675497b11a486d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:56:08 +0000 Subject: [PATCH 5/5] Fix linting errors in test_openapi_directory.py Co-authored-by: phalt <490685+phalt@users.noreply.github.com> --- test_openapi_directory.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test_openapi_directory.py b/test_openapi_directory.py index 7c94a6d..7a8632e 100644 --- a/test_openapi_directory.py +++ b/test_openapi_directory.py @@ -58,7 +58,7 @@ def test_schema_file(schema_path: pathlib.Path) -> Tuple[str, str, Exception | N # Basic validation - ensure we got a spec with some content if spec is None: return "failed", "Parsed spec is None", None - + # Check if this is a Swagger 2.x file (even if cicerone auto-converts it) # Cicerone preserves the original format in spec.raw if "swagger" in spec.raw: @@ -72,7 +72,7 @@ def test_schema_file(schema_path: pathlib.Path) -> Tuple[str, str, Exception | N return "skipped", f"Swagger {version_str} (not supported, cicerone requires OpenAPI 3.x)", None except (IndexError, ValueError): pass # If we can't parse version, continue with normal processing - + return "success", "", None except Exception as e: return "failed", f"{type(e).__name__}: {str(e)}", e @@ -181,9 +181,7 @@ def main() -> int: testable_count = len(schema_files) - skipped_count if testable_count > 0: success_rate = successes / testable_count * 100 - print( - f"Success rate: {success_rate:.2f}% ({successes}/{testable_count} testable schemas)" - ) + print(f"Success rate: {success_rate:.2f}% ({successes}/{testable_count} testable schemas)") else: # Output 0.00% for consistency with badge extraction print("Success rate: 0.00% (no testable schemas)")