Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hatch, Python 3.12, and more #440

Merged
merged 14 commits into from
Jul 29, 2024
Merged
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
Loading