Skip to content

Commit

Permalink
Merge pull request #26 from softwareone-platform/MPT-6085_add_cli_for…
Browse files Browse the repository at this point in the history
…_utils

MPT-6085 add cli for future utils; move openapi generation script to …
  • Loading branch information
ffaraone authored Jan 13, 2025
2 parents 5dffd2c + 7de8e56 commit a67ed44
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-build-merge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
retention-days: 10

- name: Generate openapi.json
run: uv run python -m scripts.generate_openapi_json openapi.json
run: uv run ffcops openapi -f json -o openapi.json

- name: Save openapi.json the artefacts
uses: actions/upload-artifact@v4
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,7 @@ bandit.json

# Postgres data
pg_data/


# OpenAPI default spec file
ffc_operations_openapi_spec.yml
4 changes: 4 additions & 0 deletions app/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from app.cli import app

if __name__ == "__main__":
app()
64 changes: 64 additions & 0 deletions app/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import json
from enum import Enum
from pathlib import Path
from typing import Annotated

import typer
import yaml
from fastapi.openapi.utils import get_openapi


class OutputFormat(str, Enum):
json = "json"
yaml = "yaml"


app = typer.Typer(
add_completion=False,
rich_markup_mode="rich",
)


@app.command()
def openapi(
output: Annotated[
Path | None,
typer.Option(
"--output",
"-o",
help="Output file",
),
] = Path("ffc_operations_openapi_spec.yml"),
output_format: Annotated[
OutputFormat,
typer.Option(
"--output-format",
"-f",
help="Output file format",
),
] = OutputFormat.yaml,
):
"""
Generates the OpenAPI spec file.
"""
from app import main

dump_fn = json.dump if output_format == OutputFormat.json else yaml.dump
spec = get_openapi(
title=main.app.title,
version=main.app.version,
openapi_version=main.app.openapi_version,
description=main.app.description,
routes=main.app.routes,
)
with open(output, "w") as f: # type: ignore
dump_fn(spec, f, indent=2)


@app.callback()
def main(
ctx: typer.Context,
):
from app import settings

ctx.obj = settings
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ dependencies = [
"uvloop==0.21.*",
"uvicorn-worker==0.3.*",
"svcs==24.1.*",
"pyyaml==6.0.*",
"typer==0.13.*",
]

[dependency-groups]
Expand All @@ -43,8 +45,17 @@ dev = [
"freezegun>=1.5.1,<2.0",
"ipdb>=0.13.13,<1.0",
"asgi-lifespan>=2.1.0,<3.0",
"types-pyyaml>=6.0.12.20241230,<7.0",
]

[project.scripts]
ffcops = "app.cli:app"

[tool.uv]
package = true

[tool.setuptools]
py-modules = ["app"]

[tool.ruff]
line-length = 100
Expand Down
25 changes: 0 additions & 25 deletions scripts/generate_openapi_json.py

This file was deleted.

2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ sonar.language=py
sonar.sources=app
sonar.tests=tests
sonar.inclusions=app/**
sonar.exclusions=tests/**, app/main.py
sonar.exclusions=tests/**, app/main.py, app/__main__.py

sonar.python.coverage.reportPaths=coverage.xml
sonar.python.bandit.reportPaths=bandit.json
Expand Down
12 changes: 6 additions & 6 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import uuid
from collections.abc import Callable
from contextlib import asynccontextmanager

import pytest
Expand All @@ -9,9 +8,10 @@

from app.auth import JWTBearer, JWTCredentials, current_system, get_current_system
from app.db.models import System
from tests.conftest import JWTTokenFactory


async def test_jwt_bearer(mocker, jwt_token_factory):
async def test_jwt_bearer(mocker: MockerFixture, jwt_token_factory: JWTTokenFactory):
bearer = JWTBearer()
request = mocker.Mock()
request.headers = {"Authorization": f"Bearer {jwt_token_factory('test', 'secret')}"}
Expand All @@ -21,7 +21,7 @@ async def test_jwt_bearer(mocker, jwt_token_factory):
assert credentials.claim["sub"] == "test"


async def test_jwt_bearer_invalid_token(mocker):
async def test_jwt_bearer_invalid_token(mocker: MockerFixture):
bearer = JWTBearer()
request = mocker.Mock()
request.headers = {"Authorization": "Bearer 1234567890"}
Expand All @@ -31,7 +31,7 @@ async def test_jwt_bearer_invalid_token(mocker):
assert exc_info.value.detail == "Unauthorized"


async def test_jwt_bearer_no_token(mocker):
async def test_jwt_bearer_no_token(mocker: MockerFixture):
bearer = JWTBearer()
request = mocker.Mock()
request.headers = {}
Expand Down Expand Up @@ -61,7 +61,7 @@ async def test_get_current_system(


async def test_get_current_system_system_not_found(
mocker: MockerFixture, jwt_token_factory: Callable[[str, str], str], db_session: AsyncSession
mocker: MockerFixture, jwt_token_factory: JWTTokenFactory, db_session: AsyncSession
):
bearer = JWTBearer()
request = mocker.Mock()
Expand All @@ -80,7 +80,7 @@ async def test_get_current_system_system_not_found(


async def test_get_current_system_invalid_subject(
mocker: MockerFixture, jwt_token_factory: Callable[[str, str], str], db_session: AsyncSession
mocker: MockerFixture, jwt_token_factory: JWTTokenFactory, db_session: AsyncSession
):
bearer = JWTBearer()
request = mocker.Mock()
Expand Down
39 changes: 39 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import json
import shlex
from pathlib import Path

import yaml
from pytest_mock import MockerFixture
from typer.testing import CliRunner

from app.cli import app


def test_openapi(mocker: MockerFixture):
spec = {"test": "openapi"}
mocker.patch("app.cli.get_openapi", return_value=spec)
mocked_open = mocker.mock_open()
mocker.patch("app.cli.open", mocked_open)

runner = CliRunner()
result = runner.invoke(app, "openapi")

assert result.exit_code == 0
mocked_open.assert_called_once_with(Path("ffc_operations_openapi_spec.yml"), "w")
written_data = "".join(call.args[0] for call in mocked_open().write.call_args_list)
assert written_data == yaml.dump(spec, indent=2)


def test_openapi_custom_output(mocker: MockerFixture):
spec = {"test": "openapi"}
mocker.patch("app.cli.get_openapi", return_value=spec)
mocked_open = mocker.mock_open()
mocker.patch("app.cli.open", mocked_open)

runner = CliRunner()
result = runner.invoke(app, shlex.split("openapi -o openapi.json -f json"))

assert result.exit_code == 0
mocked_open.assert_called_once_with(Path("openapi.json"), "w")
written_data = "".join(call.args[0] for call in mocked_open().write.call_args_list)
assert written_data == json.dumps(spec, indent=2)
1 change: 0 additions & 1 deletion tests/test_organizations_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,6 @@ async def test_create_organization_already_created(
async def test_create_organization_api_modifier_error(
mocker: MockerFixture,
httpx_mock: HTTPXMock,
mock_settings: None,
api_client: AsyncClient,
ffc_jwt_token: str,
):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


@freeze_time("2024-01-01T00:00:00Z")
def test_get_api_modifier_jwt_token(mock_settings: None):
def test_get_api_modifier_jwt_token():
token = get_api_modifier_jwt_token()
decoded_token = jwt.decode(
token,
Expand Down
17 changes: 16 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a67ed44

Please sign in to comment.