Skip to content

Commit

Permalink
added mypy and ruff and tool-versions w/ burnettk
Browse files Browse the repository at this point in the history
  • Loading branch information
jasquat committed Oct 11, 2023
1 parent 6cb2bbe commit 3c0b1e4
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 248 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ jobs:
- run: poetry install --no-interaction --no-root
if: steps.cache-deps.outputs.cache-hit != 'true'
- run: poetry install --no-interaction
- run: poetry run mypy .
- run: poetry run ruff .
- run: poetry run pytest
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.11.0
416 changes: 241 additions & 175 deletions poetry.lock

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Flask-OAuthlib = "^0.9.6"
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
connector-example = {path = "tests/mock_connectors/connector-example", develop = true}
mypy = "^1.6.0"
ruff = "^0.0.292"

[build-system]
requires = ["poetry-core"]
Expand All @@ -23,3 +25,50 @@ build-backend = "poetry.core.masonry.api"
pythonpath = [
".", "src",
]

[tool.ruff]
select = [
"B", # flake8-bugbear
"C", # mccabe
"E", # pycodestyle error
# "ERA", # eradicate
"F", # pyflakes
"N", # pep8-naming
"PL", # pylint
"S", # flake8-bandit
"UP", # pyupgrade
"W", # pycodestyle warning
"I001" # isort
]

ignore = [
"C901", # "complexity" category
"PLR", # "refactoring" category has "too many lines in method" type stuff
"PLC1901",
"PLE1205" # saw this Too many arguments for `logging` format string give a false positive once
]

line-length = 130

# target python 3.10
target-version = "py310"

exclude = [
"migrations"
]

[tool.ruff.per-file-ignores]
"migrations/versions/*.py" = ["E501"]
"tests/**/*.py" = ["PLR2004", "S101"] # PLR2004 is about magic vars, S101 allows assert

[tool.ruff.isort]
force-single-line = true

[tool.mypy]
strict = true
disallow_any_generics = false
warn_unreachable = true
pretty = true
show_column_numbers = true
show_error_codes = true
show_error_context = true
46 changes: 25 additions & 21 deletions src/spiffworkflow_proxy/blueprint.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
from flask import Blueprint, current_app
import json
import re
import typing
from typing import Any

from flask import Blueprint
from flask import Response
from flask import current_app
from flask import redirect
from flask import request
from flask import Response
from flask import session
from flask import url_for
from flask_oauthlib.contrib.client import OAuth
from flask_oauthlib.contrib.client import OAuth # type: ignore

from spiffworkflow_proxy.plugin_service import PluginService

proxy_blueprint = Blueprint('proxy_blueprint', __name__)


@proxy_blueprint.route('/')
def index():
def index() -> str:
return "This is the SpiffWorkflow Connector. Point SpiffWorkfow-backend configuration to this url." \
" Please see /v1/commands for a list of commands this connector proxy will allow."


@proxy_blueprint.route("/liveness")
def status():
def status() -> Response:
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")


@proxy_blueprint.route("/v1/commands")
def list_commands():
def list_commands() -> Response:
return list_targets(PluginService.available_commands_by_plugin())


@proxy_blueprint.route("/v1/do/<plugin_display_name>/<command_name>", methods=["GET", "POST"])
def do_command(plugin_display_name, command_name):
def do_command(plugin_display_name: str, command_name: str) -> Response:
command = PluginService.command_named(plugin_display_name, command_name)
if command is None:
return json_error_response(
f"Command not found: {plugin_display_name}:{command_name}", status=404
)

params = request.json
params = typing.cast(dict, request.json)
task_data = params.pop('spiff__task_data', '{}')

try:
Expand All @@ -59,17 +63,17 @@ def do_command(plugin_display_name, command_name):


@proxy_blueprint.route("/v1/auths")
def list_auths():
def list_auths() -> Response:
return list_targets(PluginService.available_auths_by_plugin())


@proxy_blueprint.route("/v1/auth/<plugin_display_name>/<auth_name>")
def do_auth(plugin_display_name, auth_name):
def do_auth(plugin_display_name: str, auth_name: str) -> Any:
params = request.args.to_dict()
our_redirect_url = params["redirect_url"]
session["redirect_url"] = our_redirect_url

handler = auth_handler(plugin_display_name, auth_name, params)
handler = auth_handler(plugin_display_name, auth_name)
if handler is None:
return Response("Auth not found", status=404)

Expand All @@ -88,8 +92,8 @@ def do_auth(plugin_display_name, auth_name):


@proxy_blueprint.route("/v1/auth/<plugin_display_name>/<auth_name>/callback")
def auth_callback(plugin_display_name, auth_name):
handler = auth_handler(plugin_display_name, auth_name, session)
def auth_callback(plugin_display_name: str, auth_name: str) -> Response:
handler = auth_handler(plugin_display_name, auth_name)
if handler is None:
return Response("Auth not found", status=404)

Expand All @@ -102,10 +106,10 @@ def auth_callback(plugin_display_name, auth_name):
if re.match(r".*\?.*", redirect_url):
redirect_url_params_symbol = "&"

return redirect(f"{redirect_url}{redirect_url_params_symbol}response={response}")
return redirect(f"{redirect_url}{redirect_url_params_symbol}response={response}") # type: ignore


def list_targets(targets):
def list_targets(targets: dict[str, dict[str, type]]) -> Response:
descriptions = []

for plugin_name, plugin_targets in targets.items():
Expand All @@ -118,7 +122,7 @@ def list_targets(targets):
return Response(json.dumps(descriptions), status=200, mimetype="application/json")


def auth_handler(plugin_display_name, auth_name, params):
def auth_handler(plugin_display_name: str, auth_name: str) -> Any:
auth = PluginService.auth_named(plugin_display_name, auth_name)
if auth is not None:
app_description = auth().app_description(current_app.config)
Expand All @@ -127,18 +131,18 @@ def auth_handler(plugin_display_name, auth_name, params):
# would need to expand if other auth providers are used
handler = OAuth(current_app).remote_app(**app_description)

@handler.tokengetter
def tokengetter():
@handler.tokengetter # type: ignore
def tokengetter() -> None:
pass

@handler.tokensaver
def tokensaver(token):
@handler.tokensaver # type: ignore
def tokensaver(token: str) -> None:
pass

return handler


def json_error_response(message, status):
def json_error_response(message: str, status: int) -> Response:
resp = {"error": message, "status": status}
return Response(json.dumps(resp), status=status)

68 changes: 33 additions & 35 deletions src/spiffworkflow_proxy/plugin_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import pkgutil
import types
import typing
from collections.abc import Generator
from inspect import Parameter
from typing import Any


class PluginService:
Expand All @@ -14,52 +17,46 @@ class PluginService:
PLUGIN_PREFIX: str = "connector_"

@staticmethod
def plugin_display_name(plugin_name):
def plugin_display_name(plugin_name: str) -> str:
return plugin_name.removeprefix(PluginService.PLUGIN_PREFIX)

@staticmethod
def plugin_name_from_display_name(plugin_display_name):
def plugin_name_from_display_name(plugin_display_name: str) -> str:
return PluginService.PLUGIN_PREFIX + plugin_display_name

@staticmethod
def available_plugins():
def available_plugins() -> dict[str, types.ModuleType]:
return {
name: importlib.import_module(name)
for finder, name, ispkg in pkgutil.iter_modules()
if name.startswith(PluginService.PLUGIN_PREFIX)
}

@staticmethod
def available_auths_by_plugin():
def available_auths_by_plugin() -> dict[str, dict[str, type]]:
return {
plugin_name: {
auth_name: auth
for auth_name, auth in PluginService.auths_for_plugin(
plugin_name: dict(PluginService.auths_for_plugin(
plugin_name, plugin
)
}
))
for plugin_name, plugin in PluginService.available_plugins().items()
}

@staticmethod
def available_commands_by_plugin():
def available_commands_by_plugin() -> dict[str, dict[str, type]]:
return {
plugin_name: {
command_name: command
for command_name, command in PluginService.commands_for_plugin(
plugin_name: dict(PluginService.commands_for_plugin(
plugin_name, plugin
)
}
))
for plugin_name, plugin in PluginService.available_plugins().items()
}

@staticmethod
def target_id(plugin_name, target_name):
def target_id(plugin_name: str, target_name: str) -> str:
plugin_display_name = PluginService.plugin_display_name(plugin_name)
return f"{plugin_display_name}/{target_name}"

@staticmethod
def auth_named(plugin_display_name, auth_name):
def auth_named(plugin_display_name: str, auth_name: str) -> type | None:
plugin_name = PluginService.plugin_name_from_display_name(plugin_display_name)
available_auths_by_plugin = PluginService.available_auths_by_plugin()

Expand All @@ -69,7 +66,7 @@ def auth_named(plugin_display_name, auth_name):
return None

@staticmethod
def command_named(plugin_display_name, command_name):
def command_named(plugin_display_name: str, command_name: str) -> type | None:
plugin_name = PluginService.plugin_name_from_display_name(plugin_display_name)
available_commands_by_plugin = PluginService.available_commands_by_plugin()

Expand All @@ -79,20 +76,26 @@ def command_named(plugin_display_name, command_name):
return None

@staticmethod
def modules_for_plugin_in_package(plugin, package_name):
def modules_for_plugin_in_package(
plugin: types.ModuleType, package_name: str | None
) -> Generator[tuple[str, types.ModuleType], None, None]:
for finder, name, ispkg in pkgutil.iter_modules(plugin.__path__):
if ispkg and name == package_name:
sub_pkg = finder.find_module(name).load_module(name)
yield from PluginService.modules_for_plugin_in_package(sub_pkg, None)
found_module = finder.find_module(name) # type: ignore
if found_module is not None:
sub_pkg = found_module.load_module(name)
yield from PluginService.modules_for_plugin_in_package(sub_pkg, None)
elif package_name is None:
spec = finder.find_spec(name)
spec = finder.find_spec(name) # type: ignore
if spec is not None and spec.loader is not None:
module = types.ModuleType(spec.name)
spec.loader.exec_module(module)
yield name, module

@staticmethod
def targets_for_plugin(plugin_name, plugin, target_package_name):
def targets_for_plugin(
plugin_name: str, plugin: types.ModuleType, target_package_name: str
) -> Generator[tuple[str, type], None, None]:
for module_name, module in PluginService.modules_for_plugin_in_package(
plugin, target_package_name
):
Expand All @@ -101,16 +104,16 @@ def targets_for_plugin(plugin_name, plugin, target_package_name):
yield member_name, member

@staticmethod
def auths_for_plugin(plugin_name, plugin):
def auths_for_plugin(plugin_name: str, plugin: types.ModuleType) -> Generator[tuple[str, type], None, None]:
yield from PluginService.targets_for_plugin(plugin_name, plugin, "auths")

@staticmethod
def commands_for_plugin(plugin_name, plugin):
def commands_for_plugin(plugin_name: str, plugin: types.ModuleType) -> Generator[tuple[str, type], None, None]:
# TODO check if class has an execute method before yielding
yield from PluginService.targets_for_plugin(plugin_name, plugin, "commands")

@staticmethod
def param_annotation_desc(param):
def param_annotation_desc(param: Parameter) -> dict:
"""Parses a callable parameter's type annotation, if any, to form a ParameterDescription."""
param_id = param.name
param_type_desc = "any"
Expand All @@ -129,12 +132,7 @@ def param_annotation_desc(param):
# get_args normalizes Optional[str] to (str, none)
# all unsupported types are marked so (str, dict) -> (str, unsupported)
# the absense of a type annotation results in an empty set
annotation_types = set(
map(
lambda t: t if t in supported_types else unsupported_type_marker,
typing.get_args(annotation),
)
)
annotation_types = {t if t in supported_types else unsupported_type_marker for t in typing.get_args(annotation)}

# a parameter is required if it has no default value and none is not in its type set
param_req = param.default is param.empty and none_type not in annotation_types
Expand All @@ -152,7 +150,7 @@ def param_annotation_desc(param):
return {"id": param_id, "type": param_type_desc, "required": param_req}

@staticmethod
def callable_params_desc(kallable):
def callable_params_desc(kallable: Any) -> list[dict]:
sig = inspect.signature(kallable)
params_to_skip = ["self", "kwargs"]
sig_params = filter(
Expand All @@ -163,7 +161,7 @@ def callable_params_desc(kallable):
return params

@staticmethod
def describe_target(plugin_name, target_name, target):
parameters = PluginService.callable_params_desc(target.__init__)
def describe_target(plugin_name: str, target_name: str, target: type) -> dict:
parameters = PluginService.callable_params_desc(target.__init__) # type: ignore
target_id = PluginService.target_id(plugin_name, target_name)
return {"id": target_id, "parameters": parameters}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Simple Example Command."""
import json
from decimal import Decimal
from typing import Any


class CombineStrings:
Expand All @@ -17,7 +16,7 @@ def __init__(
self.arg1 = arg1
self.arg2 = arg2

def execute(self, config, task_data):
def execute(self, config: Any, task_data: Any) -> Any:
"""Execute."""
# Get the service resource.
return self.arg1 + self.arg2
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class SomeHelper:
"""SomeHelper class, should not be returned as a command or auth."""
def _execute(self, paramA, paramB):
def _execute(self, param_a: str, param_b: str) -> None:
pass
Loading

0 comments on commit 3c0b1e4

Please sign in to comment.