Skip to content

Commit

Permalink
Merge pull request #440 from botify-labs/improvement/support-python-3.12
Browse files Browse the repository at this point in the history
  • Loading branch information
ybastide authored Jul 29, 2024
2 parents 418b5f8 + ba14ede commit c1dda3f
Show file tree
Hide file tree
Showing 16 changed files with 123 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.9"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-beta.4", "pypy-3.9"]

steps:
- uses: actions/checkout@v3
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -13,7 +13,7 @@ repos:

- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.8
rev: v0.5.5
hooks:
# Run the linter.
- id: ruff
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dynamic = ["version"]

Expand All @@ -50,10 +51,11 @@ dependencies = [
[project.optional-dependencies]
dev = [
"boto3-stubs[s3,swf]",
"cffi==v1.17.0rc1; python_full_version=='3.13.0b4'", # via cryptography via moto, secretstorage
"flaky",
"hatch==1.7.0",
"invoke",
"moto<3.0.0",
"moto>=4.2.8,<5.0.0",
"packaging",
"pre-commit",
"pytest",
Expand Down
6 changes: 5 additions & 1 deletion script/test
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/bin/bash

# not needed, but harmless, in CI/container
find . -name '*.pyc' -print0 | xargs -0 rm
export PYTHONDONTWRITEBYTECODE=1

# The AWS_DEFAULT_REGION parameter determines the region used for SWF
# Leaving it to a value different than "us-east-1" would break moto,
# because moto.swf only mocks calls to us-east-1 region for now.
Expand All @@ -26,7 +30,7 @@ export SIMPLEFLOW_VCR_RECORD_MODE=none
# Disable jumbo fields
export SIMPLEFLOW_JUMBO_FIELDS_BUCKET=""

# Prevent Travis from overriding boto configuration
# Prevent CI from overriding boto configuration
export BOTO_CONFIG=/dev/null

PYTHON=${PYTHON:-python}
Expand Down
8 changes: 4 additions & 4 deletions simpleflow/swf/mapper/actors/decider.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def complete(
raise DoesNotExistError(
f"Unable to complete decision task with token={task_token}",
message,
)
raise ResponseError(message)
) from e
raise ResponseError(message, error_code=error_code) from e
finally:
logging_context.reset()

Expand Down Expand Up @@ -117,9 +117,9 @@ def poll(self, task_list: str | None = None, identity: str | None = None, **kwar
raise DoesNotExistError(
"Unable to poll decision task",
message,
)
) from e

raise ResponseError(message)
raise ResponseError(message, error_code=error_code) from e

token = task.get("taskToken")
if not token:
Expand Down
22 changes: 11 additions & 11 deletions simpleflow/swf/mapper/actors/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ def cancel(self, task_token: str, details: str | None = None) -> dict[str, Any]
raise DoesNotExistError(
f"Unable to cancel activity task with token={task_token}",
message,
)
raise ResponseError(message)
) from e
raise ResponseError(message, error_code=error_code) from e
finally:
logging_context.reset()

Expand All @@ -87,9 +87,9 @@ def complete(self, task_token: str, result: Any = None) -> dict[str, Any] | None
raise DoesNotExistError(
f"Unable to complete activity task with token={task_token}",
message,
)
) from e

raise ResponseError(message)
raise ResponseError(message, error_code=error_code) from e
except JumboTooLargeError as e:
return self.respond_activity_task_failed(task_token, reason=format_exc(e))

Expand All @@ -113,9 +113,9 @@ def fail(self, task_token: str, details: str | None = None, reason: str | None =
raise DoesNotExistError(
f"Unable to fail activity task with token={task_token}",
message,
)
) from e

raise ResponseError(message)
raise ResponseError(message, error_code=error_code) from e
except JumboTooLargeError as e:
return self.respond_activity_task_failed(task_token, reason=format_exc(e))

Expand All @@ -137,15 +137,15 @@ def heartbeat(self, task_token: str, details: str | None = None) -> dict[str, An
raise DoesNotExistError(
f"Unable to send heartbeat with token={task_token}",
message,
)
) from e

if error_code == "ThrottlingException":
raise RateLimitExceededError(
f"Rate exceeded when sending heartbeat with token={task_token}",
message,
)
) from e

raise ResponseError(message)
raise ResponseError(message, error_code=error_code) from e

def poll(self, task_list: str | None = None, identity: str | None = None) -> Response:
"""Polls for an activity task to process from current
Expand Down Expand Up @@ -184,9 +184,9 @@ def poll(self, task_list: str | None = None, identity: str | None = None) -> Res
raise DoesNotExistError(
"Unable to poll activity task",
message,
)
) from e

raise ResponseError(message)
raise ResponseError(message, error_code=error_code) from e

if not task.get("taskToken"):
raise PollTimeout("Activity Worker poll timed out")
Expand Down
64 changes: 40 additions & 24 deletions simpleflow/swf/mapper/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import re
from collections.abc import Sequence
from functools import partial, wraps
from typing import Any, Callable
from typing import Any, Callable, Pattern

from botocore.exceptions import ClientError


class SWFError(Exception):
def __init__(self, message: str, raw_error: str = "", *args) -> None:
def __init__(self, message: str = "", raw_error: str = "", error_code: str = "", *args) -> None:
"""
Examples:
Expand Down Expand Up @@ -46,9 +46,16 @@ def __init__(self, message: str, raw_error: str = "", *args) -> None:
'kind'
>>> error.details
'details'
>>> error = SWFError('message', error_code='FooFault')
>>> error.message
'message'
>>> error.error_code
'FooFault'
>>> error.details
''
"""
Exception.__init__(self, message, *args)
super().__init__(message, *args)

values = raw_error.split(":", 1)

Expand All @@ -59,6 +66,7 @@ def __init__(self, message: str, raw_error: str = "", *args) -> None:

self.kind = values[0].strip()
self.type_ = self.kind.lower().strip().replace(" ", "_") if self.kind else None
self.error_code = error_code

@property
def message(self):
Expand Down Expand Up @@ -105,6 +113,10 @@ class RateLimitExceededError(SWFError):
pass


class WorkflowExecutionAlreadyStartedError(SWFError):
pass


def ignore(*args, **kwargs):
return

Expand All @@ -113,18 +125,15 @@ def ignore(*args, **kwargs):
REGEX_NESTED_RESOURCE = re.compile(r"Unknown (?:type|execution)[:,]\s*([^ =]+)\s*=")


def match_equals(regex, string, values):
def match_equals(regex: Pattern, string: str | None, values: str | Sequence[str]) -> bool:
"""
Extract a value from a string with a regex and compare it.
:param regex: to extract the value to check.
:type regex: _sre.SRE_Pattern (compiled regex)
:param string: that contains the value to extract.
:type string: str
:param values: to compare with.
:type values: [str]
"""
if string is None:
Expand All @@ -139,12 +148,11 @@ def match_equals(regex, string, values):
return matched[0] in values


def is_unknown_resource_raised(error, *args, **kwargs):
def is_unknown_resource_raised(error: Exception, *args, **kwargs) -> bool:
"""
Handler that checks if *error* is an unknown resource fault.
:param error: is the exception to check.
:type error: Exception
"""
if not isinstance(error, ClientError):
Expand All @@ -153,7 +161,7 @@ def is_unknown_resource_raised(error, *args, **kwargs):
return extract_error_code(error) == "UnknownResourceFault"


def is_unknown(resource: str | Sequence[str]):
def is_unknown(resource: str | Sequence[str]) -> Callable:
"""
Return a function that checks if *error* is an unknown *resource* fault.
Expand Down Expand Up @@ -185,7 +193,7 @@ def wrapped(error, *args, **kwargs):
return wrapped


def always(value):
def always(value: Any) -> Callable:
"""
Always return *value* whatever arguments it got.
Expand All @@ -212,7 +220,7 @@ def wrapped(*args, **kwargs):
return wrapped


def generate_resource_not_found_message(error):
def generate_resource_not_found_message(error: Exception) -> str:
error_code = extract_error_code(error)
if error_code != "UnknownResourceFault":
raise ValueError(f"cannot extract resource from {error}")
Expand All @@ -222,37 +230,43 @@ def generate_resource_not_found_message(error):
return f"Resource {resource[0] if resource else 'unknown'} does not exist"


def raises(exception, when, extract: Callable[[Any], str] = str):
def raises(
exception: type[Exception] | type[SWFError],
when: Callable[[Exception, tuple, dict], bool],
extract: Callable[[Any], str] = str,
):
"""
:param exception: to raise when the predicate is True.
:type exception: type(Exception)
:param when: predicate to apply.
:type when: (error, *args, **kwargs) -> bool
:param extract: function to extract the value from the exception.
"""

@wraps(raises)
def raises_closure(error, *args, **kwargs):
if when(error, *args, **kwargs) is True:
raise exception(extract(error))
raise error
if isinstance(getattr(error, "response", None), dict) and issubclass(exception, SWFError):
raise exception(extract_message(error), error_code=extract_error_code(error)) from error

raise exception(extract(error)) from error
raise error from None

return raises_closure


def catch(exceptions, handle_with=None, log=False):
def catch(
exceptions: type[Exception] | Sequence[type[Exception]] | tuple[type[Exception]],
handle_with: Callable[[Exception, tuple, dict], Any] | None = None,
log: bool = False,
):
"""
Catch *exceptions*, then eventually handle and log them.
:param exceptions: sequence of exceptions to catch.
:type exceptions: Exception | (Exception, )
:param handle_with: handle the exceptions (if handle_with is not None) or
raise them again.
:type handle_with: function(err, *args, **kwargs)
:param log: the exception with default logger.
:type log: bool
Examples:
Expand Down Expand Up @@ -307,8 +321,10 @@ def translate(exceptions, to):
"""

def throw(err, *args, **kwargs):
raise to(extract_message(err))
def throw(err: Exception, *args, **kwargs):
if isinstance(getattr(err, "response", None), dict) and issubclass(to, SWFError):
raise to(extract_message(err), error_code=extract_error_code(err)) from err
raise to(extract_message(err)) from err

return catch(exceptions, handle_with=throw)

Expand Down
8 changes: 4 additions & 4 deletions simpleflow/swf/mapper/models/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ def _diff(self, ignore_fields: list[str] | None = None) -> ModelDiff:
error_code = extract_error_code(e)
message = extract_message(e)
if error_code == "UnknownResourceFault":
raise DoesNotExistError("Remote ActivityType does not exist")
raise DoesNotExistError("Remote ActivityType does not exist") from e

raise ResponseError(message)
raise ResponseError(message, error_code=error_code) from e

info = description["typeInfo"]
config = description["configuration"]
Expand Down Expand Up @@ -192,9 +192,9 @@ def save(self):
error_code = extract_error_code(e)
message = extract_message(e)
if error_code == "TypeAlreadyExistsFault":
raise AlreadyExistsError(f"{self} already exists")
raise AlreadyExistsError(f"{self} already exists") from e
if error_code in ("UnknownResourceFault", "TypeDeprecatedFault"):
raise DoesNotExistError(f"{error_code}: {message}")
raise DoesNotExistError(f"{error_code}: {message}") from e
raise

@exceptions.catch(
Expand Down
8 changes: 4 additions & 4 deletions simpleflow/swf/mapper/models/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ def _diff(self, ignore_fields: list[str] | None = None) -> ModelDiff:
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "UnknownResourceFault":
raise DoesNotExistError("Remote Domain does not exist")
raise DoesNotExistError("Remote Domain does not exist") from e

raise ResponseError(e.args[0])
raise ResponseError(e.args[0], error_code=error_code) from e

domain_info = description["domainInfo"]
domain_config = description["configuration"]
Expand Down Expand Up @@ -137,8 +137,8 @@ def save(self) -> None:
error_code = extract_error_code(e)
message = extract_message(e)
if error_code == "DomainAlreadyExistsFault":
raise AlreadyExistsError(f"Domain {self.name} already exists amazon-side")
raise ResponseError(message)
raise AlreadyExistsError(f"Domain {self.name} already exists amazon-side") from e
raise ResponseError(message, error_code=error_code) from e

@exceptions.translate(ClientError, to=ResponseError)
@exceptions.catch(
Expand Down
Loading

0 comments on commit c1dda3f

Please sign in to comment.