TechDebt: MyPy Core (#5653)
This commit is contained in:
parent
8c9838cc8c
commit
96b2eff1bc
@ -2,15 +2,17 @@ from collections import defaultdict
|
||||
from io import BytesIO
|
||||
from botocore.awsrequest import AWSResponse
|
||||
from moto.core.exceptions import HTTPException
|
||||
from typing import Any, Dict, Callable, List, Tuple, Union, Pattern
|
||||
from .responses import TYPE_RESPONSE
|
||||
|
||||
|
||||
class MockRawResponse(BytesIO):
|
||||
def __init__(self, response_input):
|
||||
def __init__(self, response_input: Union[str, bytes]):
|
||||
if isinstance(response_input, str):
|
||||
response_input = response_input.encode("utf-8")
|
||||
super().__init__(response_input)
|
||||
|
||||
def stream(self, **kwargs): # pylint: disable=unused-argument
|
||||
def stream(self, **kwargs: Any) -> Any: # pylint: disable=unused-argument
|
||||
contents = self.read()
|
||||
while contents:
|
||||
yield contents
|
||||
@ -18,18 +20,22 @@ class MockRawResponse(BytesIO):
|
||||
|
||||
|
||||
class BotocoreStubber:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.enabled = False
|
||||
self.methods = defaultdict(list)
|
||||
self.methods: Dict[
|
||||
str, List[Tuple[Pattern[str], Callable[..., TYPE_RESPONSE]]]
|
||||
] = defaultdict(list)
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
self.methods.clear()
|
||||
|
||||
def register_response(self, method, pattern, response):
|
||||
def register_response(
|
||||
self, method: str, pattern: Pattern[str], response: Callable[..., TYPE_RESPONSE]
|
||||
) -> None:
|
||||
matchers = self.methods[method]
|
||||
matchers.append((pattern, response))
|
||||
|
||||
def __call__(self, event_name, request, **kwargs):
|
||||
def __call__(self, event_name: str, request: Any, **kwargs: Any) -> AWSResponse:
|
||||
if not self.enabled:
|
||||
return None
|
||||
|
||||
@ -41,7 +47,7 @@ class BotocoreStubber:
|
||||
matchers = self.methods.get(request.method)
|
||||
|
||||
base_url = request.url.split("?", 1)[0]
|
||||
for i, (pattern, callback) in enumerate(matchers):
|
||||
for i, (pattern, callback) in enumerate(matchers): # type: ignore[arg-type]
|
||||
if pattern.match(base_url):
|
||||
if found_index is None:
|
||||
found_index = i
|
||||
@ -62,10 +68,10 @@ class BotocoreStubber:
|
||||
)
|
||||
|
||||
except HTTPException as e:
|
||||
status = e.code
|
||||
headers = e.get_headers()
|
||||
status = e.code # type: ignore[assignment]
|
||||
headers = e.get_headers() # type: ignore[assignment]
|
||||
body = e.get_body()
|
||||
body = MockRawResponse(body)
|
||||
response = AWSResponse(request.url, status, headers, body)
|
||||
raw_response = MockRawResponse(body)
|
||||
response = AWSResponse(request.url, status, headers, raw_response)
|
||||
|
||||
return response
|
||||
|
@ -1,12 +1,14 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from .base_backend import InstanceTrackerMeta
|
||||
|
||||
|
||||
class BaseModel(metaclass=InstanceTrackerMeta):
|
||||
def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument
|
||||
def __new__(
|
||||
cls, *args: Any, **kwargs: Any # pylint: disable=unused-argument
|
||||
) -> "BaseModel":
|
||||
instance = super(BaseModel, cls).__new__(cls)
|
||||
cls.instances.append(instance)
|
||||
cls.instances.append(instance) # type: ignore[attr-defined]
|
||||
return instance
|
||||
|
||||
|
||||
@ -37,7 +39,7 @@ class CloudFormationModel(BaseModel):
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def create_from_cloudformation_json(
|
||||
def create_from_cloudformation_json( # type: ignore[misc]
|
||||
cls,
|
||||
resource_name: str,
|
||||
cloudformation_json: Dict[str, Any],
|
||||
@ -53,7 +55,7 @@ class CloudFormationModel(BaseModel):
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def update_from_cloudformation_json(
|
||||
def update_from_cloudformation_json( # type: ignore[misc]
|
||||
cls,
|
||||
original_resource: Any,
|
||||
new_resource_name: str,
|
||||
@ -70,7 +72,7 @@ class CloudFormationModel(BaseModel):
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def delete_from_cloudformation_json(
|
||||
def delete_from_cloudformation_json( # type: ignore[misc]
|
||||
cls,
|
||||
resource_name: str,
|
||||
cloudformation_json: Dict[str, Any],
|
||||
@ -92,7 +94,7 @@ class CloudFormationModel(BaseModel):
|
||||
|
||||
|
||||
class ConfigQueryModel:
|
||||
def __init__(self, backends):
|
||||
def __init__(self, backends: Any):
|
||||
"""Inits based on the resource type's backends (1 for each region if applicable)"""
|
||||
self.backends = backends
|
||||
|
||||
@ -106,7 +108,7 @@ class ConfigQueryModel:
|
||||
backend_region: Optional[str] = None,
|
||||
resource_region: Optional[str] = None,
|
||||
aggregator: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
) -> Tuple[List[Dict[str, Any]], str]:
|
||||
"""For AWS Config. This will list all of the resources of the given type and optional resource name and region.
|
||||
|
||||
This supports both aggregated and non-aggregated listing. The following notes the difference:
|
||||
@ -195,5 +197,5 @@ class ConfigQueryModel:
|
||||
class CloudWatchMetricProvider(object):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_cloudwatch_metrics(account_id: str) -> Any:
|
||||
def get_cloudwatch_metrics(account_id: str) -> Any: # type: ignore[misc]
|
||||
pass
|
||||
|
5
moto/core/common_types.py
Normal file
5
moto/core/common_types.py
Normal file
@ -0,0 +1,5 @@
|
||||
from typing import Dict, Tuple, TypeVar
|
||||
|
||||
|
||||
TYPE_RESPONSE = Tuple[int, Dict[str, str], str]
|
||||
TYPE_IF_NONE = TypeVar("TYPE_IF_NONE")
|
@ -2,8 +2,10 @@ import responses
|
||||
import types
|
||||
from io import BytesIO
|
||||
from http.client import responses as http_responses
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
from urllib.parse import urlparse
|
||||
from werkzeug.wrappers import Request
|
||||
from .responses import TYPE_RESPONSE
|
||||
|
||||
from moto.utilities.distutils_version import LooseVersion
|
||||
|
||||
@ -21,7 +23,7 @@ class CallbackResponse(responses.CallbackResponse):
|
||||
Need to subclass so we can change a couple things
|
||||
"""
|
||||
|
||||
def get_response(self, request):
|
||||
def get_response(self, request: Any) -> responses.HTTPResponse:
|
||||
"""
|
||||
Need to override this so we can pass decode_content=False
|
||||
"""
|
||||
@ -58,20 +60,22 @@ class CallbackResponse(responses.CallbackResponse):
|
||||
raise result
|
||||
|
||||
status, r_headers, body = result
|
||||
body = responses._handle_body(body)
|
||||
body_io = responses._handle_body(body)
|
||||
headers.update(r_headers)
|
||||
|
||||
return responses.HTTPResponse(
|
||||
status=status,
|
||||
reason=http_responses.get(status),
|
||||
body=body,
|
||||
body=body_io,
|
||||
headers=headers,
|
||||
preload_content=False,
|
||||
# Need to not decode_content to mimic requests
|
||||
decode_content=False,
|
||||
)
|
||||
|
||||
def _url_matches(self, url, other, match_querystring=False):
|
||||
def _url_matches(
|
||||
self, url: Any, other: Any, match_querystring: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Need to override this so we can fix querystrings breaking regex matching
|
||||
"""
|
||||
@ -83,16 +87,18 @@ class CallbackResponse(responses.CallbackResponse):
|
||||
url = responses._clean_unicode(url)
|
||||
if not isinstance(other, str):
|
||||
other = other.encode("ascii").decode("utf8")
|
||||
return self._url_matches_strict(url, other)
|
||||
return self._url_matches_strict(url, other) # type: ignore[attr-defined]
|
||||
elif isinstance(url, responses.Pattern) and url.match(other):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def not_implemented_callback(request): # pylint: disable=unused-argument
|
||||
def not_implemented_callback(
|
||||
request: Any, # pylint: disable=unused-argument
|
||||
) -> TYPE_RESPONSE:
|
||||
status = 400
|
||||
headers = {}
|
||||
headers: Dict[str, str] = {}
|
||||
response = "The method is not implemented"
|
||||
|
||||
return status, headers, response
|
||||
@ -106,10 +112,12 @@ def not_implemented_callback(request): # pylint: disable=unused-argument
|
||||
#
|
||||
# Note that, due to an outdated API we no longer support Responses <= 0.12.1
|
||||
# This method should be used for Responses 0.12.1 < .. < 0.17.0
|
||||
def _find_first_match(self, request):
|
||||
def _find_first_match(
|
||||
self: Any, request: Any
|
||||
) -> Tuple[Optional[responses.BaseResponse], List[str]]:
|
||||
matches = []
|
||||
match_failed_reasons = []
|
||||
all_possibles = self._matches + responses._default_mock._matches
|
||||
all_possibles = self._matches + responses._default_mock._matches # type: ignore[attr-defined]
|
||||
for match in all_possibles:
|
||||
match_result, reason = match.matches(request)
|
||||
if match_result:
|
||||
@ -132,7 +140,7 @@ def _find_first_match(self, request):
|
||||
return None, match_failed_reasons
|
||||
|
||||
|
||||
def get_response_mock():
|
||||
def get_response_mock() -> responses.RequestsMock:
|
||||
"""
|
||||
The responses-library is crucial in ensuring that requests to AWS are intercepted, and routed to the right backend.
|
||||
However, as our usecase is different from a 'typical' responses-user, Moto always needs some custom logic to ensure responses behaves in a way that works for us.
|
||||
@ -152,13 +160,13 @@ def get_response_mock():
|
||||
)
|
||||
else:
|
||||
responses_mock = responses.RequestsMock(assert_all_requests_are_fired=False)
|
||||
responses_mock._find_match = types.MethodType(_find_first_match, responses_mock)
|
||||
responses_mock._find_match = types.MethodType(_find_first_match, responses_mock) # type: ignore[assignment]
|
||||
|
||||
responses_mock.add_passthru("http")
|
||||
return responses_mock
|
||||
|
||||
|
||||
def reset_responses_mock(responses_mock):
|
||||
def reset_responses_mock(responses_mock: responses.RequestsMock) -> None:
|
||||
if LooseVersion(RESPONSES_VERSION) >= LooseVersion("0.17.0"):
|
||||
from .responses_custom_registry import CustomRegistry
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from jinja2 import DictLoader, Environment
|
||||
from typing import Any, Optional
|
||||
from typing import Any, List, Tuple, Optional
|
||||
import json
|
||||
|
||||
# TODO: add "<Type>Sender</Type>" to error responses below?
|
||||
@ -67,14 +67,18 @@ class RESTError(HTTPException):
|
||||
)
|
||||
self.content_type = "application/xml"
|
||||
|
||||
def get_headers(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
return {
|
||||
"X-Amzn-ErrorType": self.error_type or "UnknownError",
|
||||
"Content-Type": self.content_type,
|
||||
}
|
||||
def get_headers(
|
||||
self, *args: Any, **kwargs: Any # pylint: disable=unused-argument
|
||||
) -> List[Tuple[str, str]]:
|
||||
return [
|
||||
("X-Amzn-ErrorType", self.error_type or "UnknownError"),
|
||||
("Content-Type", self.content_type),
|
||||
]
|
||||
|
||||
def get_body(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
return self.description
|
||||
def get_body(
|
||||
self, *args: Any, **kwargs: Any # pylint: disable=unused-argument
|
||||
) -> str:
|
||||
return self.description # type: ignore[return-value]
|
||||
|
||||
|
||||
class DryRunClientError(RESTError):
|
||||
@ -86,19 +90,19 @@ class JsonRESTError(RESTError):
|
||||
self, error_type: str, message: str, template: str = "error_json", **kwargs: Any
|
||||
):
|
||||
super().__init__(error_type, message, template, **kwargs)
|
||||
self.description = json.dumps(
|
||||
self.description: str = json.dumps(
|
||||
{"__type": self.error_type, "message": self.message}
|
||||
)
|
||||
self.content_type = "application/json"
|
||||
|
||||
def get_body(self, *args, **kwargs) -> str:
|
||||
def get_body(self, *args: Any, **kwargs: Any) -> str:
|
||||
return self.description
|
||||
|
||||
|
||||
class SignatureDoesNotMatchError(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"SignatureDoesNotMatch",
|
||||
"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.",
|
||||
@ -108,7 +112,7 @@ class SignatureDoesNotMatchError(RESTError):
|
||||
class InvalidClientTokenIdError(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"InvalidClientTokenId",
|
||||
"The security token included in the request is invalid.",
|
||||
@ -118,7 +122,7 @@ class InvalidClientTokenIdError(RESTError):
|
||||
class AccessDeniedError(RESTError):
|
||||
code = 403
|
||||
|
||||
def __init__(self, user_arn, action):
|
||||
def __init__(self, user_arn: str, action: str):
|
||||
super().__init__(
|
||||
"AccessDenied",
|
||||
"User: {user_arn} is not authorized to perform: {operation}".format(
|
||||
@ -130,7 +134,7 @@ class AccessDeniedError(RESTError):
|
||||
class AuthFailureError(RESTError):
|
||||
code = 401
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"AuthFailure",
|
||||
"AWS was not able to validate the provided access credentials",
|
||||
@ -142,9 +146,12 @@ class AWSError(JsonRESTError):
|
||||
STATUS = 400
|
||||
|
||||
def __init__(
|
||||
self, message: str, exception_type: str = None, status: Optional[int] = None
|
||||
self,
|
||||
message: str,
|
||||
exception_type: Optional[str] = None,
|
||||
status: Optional[int] = None,
|
||||
):
|
||||
super().__init__(exception_type or self.TYPE, message)
|
||||
super().__init__(exception_type or self.TYPE, message) # type: ignore[arg-type]
|
||||
self.code = status or self.STATUS
|
||||
|
||||
|
||||
@ -153,7 +160,7 @@ class InvalidNextTokenException(JsonRESTError):
|
||||
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
"InvalidNextTokenException", "The nextToken provided is invalid"
|
||||
)
|
||||
@ -162,5 +169,5 @@ class InvalidNextTokenException(JsonRESTError):
|
||||
class InvalidToken(AWSError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message="Invalid token"):
|
||||
def __init__(self, message: str = "Invalid token"):
|
||||
super().__init__("Invalid Token: {}".format(message), "InvalidToken")
|
||||
|
@ -5,6 +5,7 @@ import os
|
||||
import re
|
||||
import unittest
|
||||
from types import FunctionType
|
||||
from typing import Any, Callable, Dict, Optional, Set, TypeVar
|
||||
from unittest.mock import patch
|
||||
|
||||
import boto3
|
||||
@ -14,7 +15,7 @@ from botocore.config import Config
|
||||
from botocore.handlers import BUILTIN_HANDLERS
|
||||
|
||||
from moto import settings
|
||||
from moto.core.base_backend import BackendDict
|
||||
from .base_backend import BackendDict
|
||||
from .botocore_stubber import BotocoreStubber
|
||||
from .custom_responses_mock import (
|
||||
get_response_mock,
|
||||
@ -24,13 +25,14 @@ from .custom_responses_mock import (
|
||||
)
|
||||
|
||||
DEFAULT_ACCOUNT_ID = "123456789012"
|
||||
CALLABLE_RETURN = TypeVar("CALLABLE_RETURN")
|
||||
|
||||
|
||||
class BaseMockAWS:
|
||||
nested_count = 0
|
||||
mocks_active = False
|
||||
|
||||
def __init__(self, backends):
|
||||
def __init__(self, backends: BackendDict):
|
||||
from moto.instance_metadata import instance_metadata_backends
|
||||
from moto.moto_api._internal.models import moto_api_backend
|
||||
|
||||
@ -55,25 +57,25 @@ class BaseMockAWS:
|
||||
"AWS_ACCESS_KEY_ID": "foobar_key",
|
||||
"AWS_SECRET_ACCESS_KEY": "foobar_secret",
|
||||
}
|
||||
self.ORIG_KEYS = {}
|
||||
self.ORIG_KEYS: Dict[str, Optional[str]] = {}
|
||||
self.default_session_mock = patch("boto3.DEFAULT_SESSION", None)
|
||||
|
||||
if self.__class__.nested_count == 0:
|
||||
self.reset()
|
||||
self.reset() # type: ignore[attr-defined]
|
||||
|
||||
def __call__(self, func, reset=True):
|
||||
def __call__(self, func: Callable[..., Any], reset: bool = True) -> Any:
|
||||
if inspect.isclass(func):
|
||||
return self.decorate_class(func)
|
||||
return self.decorate_callable(func, reset)
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> "BaseMockAWS":
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self.stop()
|
||||
|
||||
def start(self, reset=True):
|
||||
def start(self, reset: bool = True) -> None:
|
||||
if not self.__class__.mocks_active:
|
||||
self.default_session_mock.start()
|
||||
self.mock_env_variables()
|
||||
@ -84,9 +86,9 @@ class BaseMockAWS:
|
||||
for backend in self.backends.values():
|
||||
backend.reset()
|
||||
|
||||
self.enable_patching(reset)
|
||||
self.enable_patching(reset) # type: ignore[attr-defined]
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
self.__class__.nested_count -= 1
|
||||
|
||||
if self.__class__.nested_count < 0:
|
||||
@ -102,10 +104,12 @@ class BaseMockAWS:
|
||||
pass
|
||||
self.unmock_env_variables()
|
||||
self.__class__.mocks_active = False
|
||||
self.disable_patching()
|
||||
self.disable_patching() # type: ignore[attr-defined]
|
||||
|
||||
def decorate_callable(self, func, reset):
|
||||
def wrapper(*args, **kwargs):
|
||||
def decorate_callable(
|
||||
self, func: Callable[..., CALLABLE_RETURN], reset: bool
|
||||
) -> Callable[..., CALLABLE_RETURN]:
|
||||
def wrapper(*args: Any, **kwargs: Any) -> CALLABLE_RETURN:
|
||||
self.start(reset=reset)
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
@ -114,10 +118,10 @@ class BaseMockAWS:
|
||||
return result
|
||||
|
||||
functools.update_wrapper(wrapper, func)
|
||||
wrapper.__wrapped__ = func
|
||||
wrapper.__wrapped__ = func # type: ignore[attr-defined]
|
||||
return wrapper
|
||||
|
||||
def decorate_class(self, klass):
|
||||
def decorate_class(self, klass: type) -> object:
|
||||
direct_methods = get_direct_methods_of(klass)
|
||||
defined_classes = set(
|
||||
x for x, y in klass.__dict__.items() if inspect.isclass(y)
|
||||
@ -181,7 +185,7 @@ class BaseMockAWS:
|
||||
continue
|
||||
return klass
|
||||
|
||||
def mock_env_variables(self):
|
||||
def mock_env_variables(self) -> None:
|
||||
# "Mock" the AWS credentials as they can't be mocked in Botocore currently
|
||||
# self.env_variables_mocks = mock.patch.dict(os.environ, FAKE_KEYS)
|
||||
# self.env_variables_mocks.start()
|
||||
@ -189,7 +193,7 @@ class BaseMockAWS:
|
||||
self.ORIG_KEYS[k] = os.environ.get(k, None)
|
||||
os.environ[k] = v
|
||||
|
||||
def unmock_env_variables(self):
|
||||
def unmock_env_variables(self) -> None:
|
||||
# This doesn't work in Python2 - for some reason, unmocking clears the entire os.environ dict
|
||||
# Obviously bad user experience, and also breaks pytest - as it uses PYTEST_CURRENT_TEST as an env var
|
||||
# self.env_variables_mocks.stop()
|
||||
@ -200,7 +204,7 @@ class BaseMockAWS:
|
||||
del os.environ[k]
|
||||
|
||||
|
||||
def get_direct_methods_of(klass):
|
||||
def get_direct_methods_of(klass: object) -> Set[str]:
|
||||
return set(
|
||||
x
|
||||
for x, y in klass.__dict__.items()
|
||||
@ -232,7 +236,7 @@ botocore_stubber = BotocoreStubber()
|
||||
BUILTIN_HANDLERS.append(("before-send", botocore_stubber))
|
||||
|
||||
|
||||
def patch_client(client):
|
||||
def patch_client(client: botocore.client.BaseClient) -> None:
|
||||
"""
|
||||
Explicitly patch a boto3-client
|
||||
"""
|
||||
@ -254,7 +258,7 @@ def patch_client(client):
|
||||
raise Exception(f"Argument {client} should be of type boto3.client")
|
||||
|
||||
|
||||
def patch_resource(resource):
|
||||
def patch_resource(resource: Any) -> None:
|
||||
"""
|
||||
Explicitly patch a boto3-resource
|
||||
"""
|
||||
@ -267,11 +271,13 @@ def patch_resource(resource):
|
||||
|
||||
|
||||
class BotocoreEventMockAWS(BaseMockAWS):
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
botocore_stubber.reset()
|
||||
reset_responses_mock(responses_mock)
|
||||
|
||||
def enable_patching(self, reset=True): # pylint: disable=unused-argument
|
||||
def enable_patching(
|
||||
self, reset: bool = True # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
# Circumvent circular imports
|
||||
from .utils import convert_flask_to_responses_response
|
||||
|
||||
@ -313,7 +319,7 @@ class BotocoreEventMockAWS(BaseMockAWS):
|
||||
)
|
||||
)
|
||||
|
||||
def disable_patching(self):
|
||||
def disable_patching(self) -> None:
|
||||
botocore_stubber.enabled = False
|
||||
self.reset()
|
||||
|
||||
@ -330,14 +336,13 @@ class ServerModeMockAWS(BaseMockAWS):
|
||||
|
||||
RESET_IN_PROGRESS = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
self.test_server_mode_endpoint = settings.test_server_mode_endpoint()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
call_reset_api = os.environ.get("MOTO_CALL_RESET_API")
|
||||
call_reset_api = not call_reset_api or call_reset_api.lower() != "false"
|
||||
if call_reset_api:
|
||||
if not call_reset_api or call_reset_api.lower() != "false":
|
||||
if not ServerModeMockAWS.RESET_IN_PROGRESS:
|
||||
ServerModeMockAWS.RESET_IN_PROGRESS = True
|
||||
import requests
|
||||
@ -345,14 +350,14 @@ class ServerModeMockAWS(BaseMockAWS):
|
||||
requests.post(f"{self.test_server_mode_endpoint}/moto-api/reset")
|
||||
ServerModeMockAWS.RESET_IN_PROGRESS = False
|
||||
|
||||
def enable_patching(self, reset=True):
|
||||
def enable_patching(self, reset: bool = True) -> None:
|
||||
if self.__class__.nested_count == 1 and reset:
|
||||
# Just started
|
||||
self.reset()
|
||||
|
||||
from boto3 import client as real_boto3_client, resource as real_boto3_resource
|
||||
|
||||
def fake_boto3_client(*args, **kwargs):
|
||||
def fake_boto3_client(*args: Any, **kwargs: Any) -> botocore.client.BaseClient:
|
||||
region = self._get_region(*args, **kwargs)
|
||||
if region:
|
||||
if "config" in kwargs:
|
||||
@ -364,7 +369,7 @@ class ServerModeMockAWS(BaseMockAWS):
|
||||
kwargs["endpoint_url"] = self.test_server_mode_endpoint
|
||||
return real_boto3_client(*args, **kwargs)
|
||||
|
||||
def fake_boto3_resource(*args, **kwargs):
|
||||
def fake_boto3_resource(*args: Any, **kwargs: Any) -> Any:
|
||||
if "endpoint_url" not in kwargs:
|
||||
kwargs["endpoint_url"] = self.test_server_mode_endpoint
|
||||
return real_boto3_resource(*args, **kwargs)
|
||||
@ -374,7 +379,7 @@ class ServerModeMockAWS(BaseMockAWS):
|
||||
self._client_patcher.start()
|
||||
self._resource_patcher.start()
|
||||
|
||||
def _get_region(self, *args, **kwargs):
|
||||
def _get_region(self, *args: Any, **kwargs: Any) -> Optional[str]:
|
||||
if "region_name" in kwargs:
|
||||
return kwargs["region_name"]
|
||||
if type(args) == tuple and len(args) == 2:
|
||||
@ -382,7 +387,7 @@ class ServerModeMockAWS(BaseMockAWS):
|
||||
return region
|
||||
return None
|
||||
|
||||
def disable_patching(self):
|
||||
def disable_patching(self) -> None:
|
||||
if self._client_patcher:
|
||||
self._client_patcher.stop()
|
||||
self._resource_patcher.stop()
|
||||
@ -394,9 +399,9 @@ class base_decorator:
|
||||
def __init__(self, backends: BackendDict):
|
||||
self.backends = backends
|
||||
|
||||
def __call__(self, func=None):
|
||||
def __call__(self, func: Optional[Callable[..., Any]] = None) -> BaseMockAWS:
|
||||
if settings.TEST_SERVER_MODE:
|
||||
mocked_backend = ServerModeMockAWS(self.backends)
|
||||
mocked_backend: BaseMockAWS = ServerModeMockAWS(self.backends)
|
||||
else:
|
||||
mocked_backend = self.mock_backend(self.backends)
|
||||
|
||||
|
@ -11,11 +11,22 @@ import xmltodict
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
from moto import settings
|
||||
from moto.core.common_types import TYPE_RESPONSE, TYPE_IF_NONE
|
||||
from moto.core.exceptions import DryRunClientError
|
||||
from moto.core.utils import camelcase_to_underscores, method_names_from_class
|
||||
from moto.utilities.utils import load_resource
|
||||
from jinja2 import Environment, DictLoader, Template
|
||||
from typing import Dict, Union, Any, Tuple, TypeVar
|
||||
from typing import (
|
||||
Dict,
|
||||
Union,
|
||||
Any,
|
||||
Tuple,
|
||||
Optional,
|
||||
List,
|
||||
Set,
|
||||
ClassVar,
|
||||
Callable,
|
||||
)
|
||||
from urllib.parse import parse_qs, parse_qsl, urlparse
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from xml.dom.minidom import parseString as parseXML
|
||||
@ -23,29 +34,19 @@ from xml.dom.minidom import parseString as parseXML
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
JINJA_ENVS = {}
|
||||
|
||||
TYPE_RESPONSE = Tuple[int, Dict[str, str], str]
|
||||
TYPE_IF_NONE = TypeVar("TYPE_IF_NONE")
|
||||
JINJA_ENVS: Dict[type, Environment] = {}
|
||||
|
||||
|
||||
def _decode_dict(d):
|
||||
decoded = OrderedDict()
|
||||
def _decode_dict(d: Dict[Any, Any]) -> Dict[str, Any]:
|
||||
decoded: Dict[str, Any] = OrderedDict()
|
||||
for key, value in d.items():
|
||||
if isinstance(key, bytes):
|
||||
newkey = key.decode("utf-8")
|
||||
elif isinstance(key, (list, tuple)):
|
||||
newkey = []
|
||||
for k in key:
|
||||
if isinstance(k, bytes):
|
||||
newkey.append(k.decode("utf-8"))
|
||||
else:
|
||||
newkey.append(k)
|
||||
else:
|
||||
newkey = key
|
||||
|
||||
if isinstance(value, bytes):
|
||||
newvalue = value.decode("utf-8")
|
||||
decoded[newkey] = value.decode("utf-8")
|
||||
elif isinstance(value, (list, tuple)):
|
||||
newvalue = []
|
||||
for v in value:
|
||||
@ -53,18 +54,18 @@ def _decode_dict(d):
|
||||
newvalue.append(v.decode("utf-8"))
|
||||
else:
|
||||
newvalue.append(v)
|
||||
else:
|
||||
newvalue = value
|
||||
|
||||
decoded[newkey] = newvalue
|
||||
else:
|
||||
decoded[newkey] = value
|
||||
|
||||
return decoded
|
||||
|
||||
|
||||
class DynamicDictLoader(DictLoader):
|
||||
def update(self, mapping):
|
||||
self.mapping.update(mapping)
|
||||
def update(self, mapping: Dict[str, str]) -> None:
|
||||
self.mapping.update(mapping) # type: ignore[attr-defined]
|
||||
|
||||
def contains(self, template):
|
||||
def contains(self, template: str) -> bool:
|
||||
return bool(template in self.mapping)
|
||||
|
||||
|
||||
@ -73,12 +74,12 @@ class _TemplateEnvironmentMixin(object):
|
||||
RIGHT_PATTERN = re.compile(r">[\s\n]+")
|
||||
|
||||
@property
|
||||
def should_autoescape(self):
|
||||
def should_autoescape(self) -> bool:
|
||||
# Allow for subclass to overwrite
|
||||
return False
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
def environment(self) -> Environment:
|
||||
key = type(self)
|
||||
try:
|
||||
environment = JINJA_ENVS[key]
|
||||
@ -94,11 +95,11 @@ class _TemplateEnvironmentMixin(object):
|
||||
|
||||
return environment
|
||||
|
||||
def contains_template(self, template_id):
|
||||
return self.environment.loader.contains(template_id)
|
||||
def contains_template(self, template_id: str) -> bool:
|
||||
return self.environment.loader.contains(template_id) # type: ignore[union-attr]
|
||||
|
||||
@classmethod
|
||||
def _make_template_id(cls, source):
|
||||
def _make_template_id(cls, source: str) -> str:
|
||||
"""
|
||||
Return a numeric string that's unique for the lifetime of the source.
|
||||
|
||||
@ -117,47 +118,49 @@ class _TemplateEnvironmentMixin(object):
|
||||
xml = re.sub(
|
||||
self.RIGHT_PATTERN, ">", re.sub(self.LEFT_PATTERN, "<", source)
|
||||
)
|
||||
self.environment.loader.update({template_id: xml})
|
||||
self.environment.loader.update({template_id: xml}) # type: ignore[union-attr]
|
||||
return self.environment.get_template(template_id)
|
||||
|
||||
|
||||
class ActionAuthenticatorMixin(object):
|
||||
|
||||
request_count = 0
|
||||
request_count: ClassVar[int] = 0
|
||||
|
||||
def _authenticate_and_authorize_action(self, iam_request_cls):
|
||||
def _authenticate_and_authorize_action(self, iam_request_cls: type) -> None:
|
||||
if (
|
||||
ActionAuthenticatorMixin.request_count
|
||||
>= settings.INITIAL_NO_AUTH_ACTION_COUNT
|
||||
):
|
||||
iam_request = iam_request_cls(
|
||||
account_id=self.current_account,
|
||||
method=self.method,
|
||||
path=self.path,
|
||||
data=self.data,
|
||||
headers=self.headers,
|
||||
account_id=self.current_account, # type: ignore[attr-defined]
|
||||
method=self.method, # type: ignore[attr-defined]
|
||||
path=self.path, # type: ignore[attr-defined]
|
||||
data=self.data, # type: ignore[attr-defined]
|
||||
headers=self.headers, # type: ignore[attr-defined]
|
||||
)
|
||||
iam_request.check_signature()
|
||||
iam_request.check_action_permitted()
|
||||
else:
|
||||
ActionAuthenticatorMixin.request_count += 1
|
||||
|
||||
def _authenticate_and_authorize_normal_action(self):
|
||||
def _authenticate_and_authorize_normal_action(self) -> None:
|
||||
from moto.iam.access_control import IAMRequest
|
||||
|
||||
self._authenticate_and_authorize_action(IAMRequest)
|
||||
|
||||
def _authenticate_and_authorize_s3_action(self):
|
||||
def _authenticate_and_authorize_s3_action(self) -> None:
|
||||
from moto.iam.access_control import S3IAMRequest
|
||||
|
||||
self._authenticate_and_authorize_action(S3IAMRequest)
|
||||
|
||||
@staticmethod
|
||||
def set_initial_no_auth_action_count(initial_no_auth_action_count):
|
||||
def set_initial_no_auth_action_count(initial_no_auth_action_count: int) -> Callable[..., Callable[..., TYPE_RESPONSE]]: # type: ignore[misc]
|
||||
_test_server_mode_endpoint = settings.test_server_mode_endpoint()
|
||||
|
||||
def decorator(function):
|
||||
def wrapper(*args, **kwargs):
|
||||
def decorator(
|
||||
function: Callable[..., TYPE_RESPONSE]
|
||||
) -> Callable[..., TYPE_RESPONSE]:
|
||||
def wrapper(*args: Any, **kwargs: Any) -> TYPE_RESPONSE:
|
||||
if settings.TEST_SERVER_MODE:
|
||||
response = requests.post(
|
||||
f"{_test_server_mode_endpoint}/moto-api/reset-auth",
|
||||
@ -191,7 +194,7 @@ class ActionAuthenticatorMixin(object):
|
||||
return result
|
||||
|
||||
functools.update_wrapper(wrapper, function)
|
||||
wrapper.__wrapped__ = function
|
||||
wrapper.__wrapped__ = function # type: ignore[attr-defined]
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@ -213,12 +216,12 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
)
|
||||
aws_service_spec = None
|
||||
|
||||
def __init__(self, service_name=None) -> None:
|
||||
def __init__(self, service_name: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.service_name = service_name
|
||||
|
||||
@classmethod
|
||||
def dispatch(cls, *args: Any, **kwargs: Any) -> Any:
|
||||
def dispatch(cls, *args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
|
||||
return cls()._dispatch(*args, **kwargs)
|
||||
|
||||
def setup_class(
|
||||
@ -227,7 +230,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
"""
|
||||
use_raw_body: Use incoming bytes if True, encode to string otherwise
|
||||
"""
|
||||
querystring = OrderedDict()
|
||||
querystring: Dict[str, Any] = OrderedDict()
|
||||
if hasattr(request, "body"):
|
||||
# Boto
|
||||
self.body = request.body
|
||||
@ -292,7 +295,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
self.data = querystring
|
||||
self.method = request.method
|
||||
self.region = self.get_region_from_url(request, full_url)
|
||||
self.uri_match = None
|
||||
self.uri_match: Optional[re.Match[str]] = None
|
||||
|
||||
self.headers = request.headers
|
||||
if "host" not in self.headers:
|
||||
@ -307,11 +310,11 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
mark_account_as_visited(
|
||||
account_id=self.current_account,
|
||||
access_key=self.access_key,
|
||||
service=self.service_name,
|
||||
service=self.service_name, # type: ignore[arg-type]
|
||||
region=self.region,
|
||||
)
|
||||
|
||||
def get_region_from_url(self, request, full_url):
|
||||
def get_region_from_url(self, request: Any, full_url: str) -> str:
|
||||
url_match = self.region_regex.search(full_url)
|
||||
user_agent_match = self.region_from_useragent_regex.search(
|
||||
request.headers.get("User-Agent", "")
|
||||
@ -329,7 +332,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
region = self.default_region
|
||||
return region
|
||||
|
||||
def get_access_key(self):
|
||||
def get_access_key(self) -> str:
|
||||
"""
|
||||
Returns the access key id used in this request as the current user id
|
||||
"""
|
||||
@ -339,11 +342,11 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
return match.group(1)
|
||||
|
||||
if self.querystring.get("AWSAccessKeyId"):
|
||||
return self.querystring.get("AWSAccessKeyId")[0]
|
||||
return self.querystring["AWSAccessKeyId"][0]
|
||||
else:
|
||||
return "AKIAEXAMPLE"
|
||||
|
||||
def get_current_account(self):
|
||||
def get_current_account(self) -> str:
|
||||
# PRIO 1: Check if we have a Environment Variable set
|
||||
if "MOTO_ACCOUNT_ID" in os.environ:
|
||||
return os.environ["MOTO_ACCOUNT_ID"]
|
||||
@ -358,11 +361,11 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
|
||||
return get_account_id_from(self.get_access_key())
|
||||
|
||||
def _dispatch(self, request, full_url, headers):
|
||||
def _dispatch(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:
|
||||
self.setup_class(request, full_url, headers)
|
||||
return self.call_action()
|
||||
|
||||
def uri_to_regexp(self, uri):
|
||||
def uri_to_regexp(self, uri: str) -> str:
|
||||
"""converts uri w/ placeholder to regexp
|
||||
'/cars/{carName}/drivers/{DriverName}'
|
||||
-> '^/cars/.*/drivers/[^/]*$'
|
||||
@ -372,7 +375,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
|
||||
"""
|
||||
|
||||
def _convert(elem, is_last):
|
||||
def _convert(elem: str, is_last: bool) -> str:
|
||||
if not re.match("^{.*}$", elem):
|
||||
return elem
|
||||
name = (
|
||||
@ -394,7 +397,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
)
|
||||
return regexp
|
||||
|
||||
def _get_action_from_method_and_request_uri(self, method, request_uri):
|
||||
def _get_action_from_method_and_request_uri(
|
||||
self, method: str, request_uri: str
|
||||
) -> str:
|
||||
"""basically used for `rest-json` APIs
|
||||
You can refer to example from link below
|
||||
https://github.com/boto/botocore/blob/develop/botocore/data/iot/2015-05-28/service-2.json
|
||||
@ -406,7 +411,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
|
||||
# make cache if it does not exist yet
|
||||
if not hasattr(self, "method_urls"):
|
||||
self.method_urls = defaultdict(lambda: defaultdict(str))
|
||||
self.method_urls: Dict[str, Dict[str, str]] = defaultdict(
|
||||
lambda: defaultdict(str)
|
||||
)
|
||||
op_names = conn._service_model.operation_names
|
||||
for op_name in op_names:
|
||||
op_model = conn._service_model.operation_model(op_name)
|
||||
@ -419,9 +426,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
self.uri_match = match
|
||||
if match:
|
||||
return name
|
||||
return None
|
||||
return None # type: ignore[return-value]
|
||||
|
||||
def _get_action(self):
|
||||
def _get_action(self) -> str:
|
||||
action = self.querystring.get("Action", [""])[0]
|
||||
if action:
|
||||
return action
|
||||
@ -433,7 +440,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
# get action from method and uri
|
||||
return self._get_action_from_method_and_request_uri(self.method, self.path)
|
||||
|
||||
def call_action(self):
|
||||
def call_action(self) -> TYPE_RESPONSE:
|
||||
headers = self.response_headers
|
||||
|
||||
try:
|
||||
@ -449,9 +456,11 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
try:
|
||||
response = method()
|
||||
except HTTPException as http_error:
|
||||
response_headers = dict(http_error.get_headers() or [])
|
||||
response_headers["status"] = http_error.code
|
||||
response = http_error.description, response_headers
|
||||
response_headers: Dict[str, Union[str, int]] = dict(
|
||||
http_error.get_headers() or []
|
||||
)
|
||||
response_headers["status"] = http_error.code # type: ignore[assignment]
|
||||
response = http_error.description, response_headers # type: ignore[assignment]
|
||||
|
||||
if isinstance(response, str):
|
||||
return 200, headers, response
|
||||
@ -466,7 +475,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _send_response(headers, response):
|
||||
def _send_response(headers: Dict[str, str], response: Any) -> Tuple[int, Dict[str, str], str]: # type: ignore[misc]
|
||||
if response is None:
|
||||
response = "", {}
|
||||
if len(response) == 2:
|
||||
@ -480,7 +489,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
headers["status"] = str(headers["status"])
|
||||
return status, headers, body
|
||||
|
||||
def _get_param(self, param_name, if_none=None) -> Any:
|
||||
def _get_param(self, param_name: str, if_none: Any = None) -> Any:
|
||||
val = self.querystring.get(param_name)
|
||||
if val is not None:
|
||||
return val[0]
|
||||
@ -503,7 +512,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
return if_none
|
||||
|
||||
def _get_int_param(
|
||||
self, param_name, if_none: TYPE_IF_NONE = None
|
||||
self, param_name: str, if_none: TYPE_IF_NONE = None # type: ignore[assignment]
|
||||
) -> Union[int, TYPE_IF_NONE]:
|
||||
val = self._get_param(param_name)
|
||||
if val is not None:
|
||||
@ -511,7 +520,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
return if_none
|
||||
|
||||
def _get_bool_param(
|
||||
self, param_name, if_none: TYPE_IF_NONE = None
|
||||
self, param_name: str, if_none: TYPE_IF_NONE = None # type: ignore[assignment]
|
||||
) -> Union[bool, TYPE_IF_NONE]:
|
||||
val = self._get_param(param_name)
|
||||
if val is not None:
|
||||
@ -522,12 +531,15 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
return False
|
||||
return if_none
|
||||
|
||||
def _get_multi_param_dict(self, param_prefix) -> Dict:
|
||||
def _get_multi_param_dict(self, param_prefix: str) -> Dict[str, Any]:
|
||||
return self._get_multi_param_helper(param_prefix, skip_result_conversion=True)
|
||||
|
||||
def _get_multi_param_helper(
|
||||
self, param_prefix, skip_result_conversion=False, tracked_prefixes=None
|
||||
):
|
||||
self,
|
||||
param_prefix: str,
|
||||
skip_result_conversion: bool = False,
|
||||
tracked_prefixes: Optional[Set[str]] = None,
|
||||
) -> Any:
|
||||
value_dict = dict()
|
||||
tracked_prefixes = (
|
||||
tracked_prefixes or set()
|
||||
@ -589,11 +601,13 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
if len(parts) != 2 or parts[1] != "member":
|
||||
value_dict[parts[0]] = value_dict.pop(k)
|
||||
else:
|
||||
value_dict = list(value_dict.values())[0]
|
||||
value_dict = list(value_dict.values())[0] # type: ignore[assignment]
|
||||
|
||||
return value_dict
|
||||
|
||||
def _get_multi_param(self, param_prefix, skip_result_conversion=False) -> Any:
|
||||
def _get_multi_param(
|
||||
self, param_prefix: str, skip_result_conversion: bool = False
|
||||
) -> List[Any]:
|
||||
"""
|
||||
Given a querystring of ?LaunchConfigurationNames.member.1=my-test-1&LaunchConfigurationNames.member.2=my-test-2
|
||||
this will return ['my-test-1', 'my-test-2']
|
||||
@ -616,7 +630,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
|
||||
return values
|
||||
|
||||
def _get_dict_param(self, param_prefix) -> Dict:
|
||||
def _get_dict_param(self, param_prefix: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Given a parameter dict of
|
||||
{
|
||||
@ -630,7 +644,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
"instance_count": "1",
|
||||
}
|
||||
"""
|
||||
params = {}
|
||||
params: Dict[str, Any] = {}
|
||||
for key, value in self.querystring.items():
|
||||
if key.startswith(param_prefix):
|
||||
params[camelcase_to_underscores(key.replace(param_prefix, ""))] = value[
|
||||
@ -638,7 +652,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
]
|
||||
return params
|
||||
|
||||
def _get_params(self) -> Any:
|
||||
def _get_params(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Given a querystring of
|
||||
{
|
||||
@ -674,12 +688,12 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
]
|
||||
}
|
||||
"""
|
||||
params = {}
|
||||
params: Dict[str, Any] = {}
|
||||
for k, v in sorted(self.querystring.items()):
|
||||
self._parse_param(k, v[0], params)
|
||||
return params
|
||||
|
||||
def _parse_param(self, key, value, params):
|
||||
def _parse_param(self, key: str, value: str, params: Any) -> None:
|
||||
keylist = key.split(".")
|
||||
obj = params
|
||||
for i, key in enumerate(keylist[:-1]):
|
||||
@ -713,7 +727,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
else:
|
||||
obj[keylist[-1]] = value
|
||||
|
||||
def _get_list_prefix(self, param_prefix: str) -> Any:
|
||||
def _get_list_prefix(self, param_prefix: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Given a query dict like
|
||||
{
|
||||
@ -752,7 +766,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
param_index += 1
|
||||
return results
|
||||
|
||||
def _get_map_prefix(self, param_prefix, key_end=".key", value_end=".value"):
|
||||
def _get_map_prefix(
|
||||
self, param_prefix: str, key_end: str = ".key", value_end: str = ".value"
|
||||
) -> Dict[str, Any]:
|
||||
results = {}
|
||||
param_index = 1
|
||||
while 1:
|
||||
@ -774,7 +790,9 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
|
||||
return results
|
||||
|
||||
def _get_object_map(self, prefix, name="Name", value="Value"):
|
||||
def _get_object_map(
|
||||
self, prefix: str, name: str = "Name", value: str = "Value"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Given a query dict like
|
||||
{
|
||||
@ -822,19 +840,16 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
return object_map
|
||||
|
||||
@property
|
||||
def request_json(self):
|
||||
def request_json(self) -> bool:
|
||||
return "JSON" in self.querystring.get("ContentType", [])
|
||||
|
||||
def error_on_dryrun(self):
|
||||
def error_on_dryrun(self) -> None:
|
||||
self.is_not_dryrun()
|
||||
|
||||
def is_not_dryrun(self, action=None):
|
||||
action = action or self._get_param("Action")
|
||||
def is_not_dryrun(self, action: Optional[str] = None) -> bool:
|
||||
if "true" in self.querystring.get("DryRun", ["false"]):
|
||||
message = (
|
||||
"An error occurred (DryRunOperation) when calling the %s operation: Request would have succeeded, but DryRun flag is set"
|
||||
% action
|
||||
)
|
||||
a = action or self._get_param("Action")
|
||||
message = f"An error occurred (DryRunOperation) when calling the {a} operation: Request would have succeeded, but DryRun flag is set"
|
||||
raise DryRunClientError(error_type="DryRunOperation", message=message)
|
||||
return True
|
||||
|
||||
@ -842,20 +857,20 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
|
||||
class _RecursiveDictRef(object):
|
||||
"""Store a recursive reference to dict."""
|
||||
|
||||
def __init__(self):
|
||||
self.key = None
|
||||
self.dic = {}
|
||||
def __init__(self) -> None:
|
||||
self.key: Optional[str] = None
|
||||
self.dic: Dict[str, Any] = {}
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "{!r}".format(self.dic)
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self.dic.__getattr__(key)
|
||||
def __getattr__(self, key: str) -> Any:
|
||||
return self.dic.__getattr__(key) # type: ignore[attr-defined]
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.dic.__getitem__(key)
|
||||
|
||||
def set_reference(self, key, dic):
|
||||
def set_reference(self, key: str, dic: Dict[str, Any]) -> None:
|
||||
"""Set the RecursiveDictRef object to keep reference to dict object
|
||||
(dic) at the key.
|
||||
|
||||
@ -877,7 +892,7 @@ class AWSServiceSpec(object):
|
||||
self.operations = spec["operations"]
|
||||
self.shapes = spec["shapes"]
|
||||
|
||||
def input_spec(self, operation):
|
||||
def input_spec(self, operation: str) -> Dict[str, Any]:
|
||||
try:
|
||||
op = self.operations[operation]
|
||||
except KeyError:
|
||||
@ -887,7 +902,7 @@ class AWSServiceSpec(object):
|
||||
shape = self.shapes[op["input"]["shape"]]
|
||||
return self._expand(shape)
|
||||
|
||||
def output_spec(self, operation):
|
||||
def output_spec(self, operation: str) -> Dict[str, Any]:
|
||||
"""Produce a JSON with a valid API response syntax for operation, but
|
||||
with type information. Each node represented by a key has the
|
||||
value containing field type, e.g.,
|
||||
@ -904,11 +919,13 @@ class AWSServiceSpec(object):
|
||||
shape = self.shapes[op["output"]["shape"]]
|
||||
return self._expand(shape)
|
||||
|
||||
def _expand(self, shape):
|
||||
def expand(dic, seen=None):
|
||||
def _expand(self, shape: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def expand(
|
||||
dic: Dict[str, Any], seen: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
seen = seen or {}
|
||||
if dic["type"] == "structure":
|
||||
nodes = {}
|
||||
nodes: Dict[str, Any] = {}
|
||||
for k, v in dic["members"].items():
|
||||
seen_till_here = dict(seen)
|
||||
if k in seen_till_here:
|
||||
@ -932,7 +949,7 @@ class AWSServiceSpec(object):
|
||||
|
||||
elif dic["type"] == "map":
|
||||
seen_till_here = dict(seen)
|
||||
node = {"type": "map"}
|
||||
node: Dict[str, Any] = {"type": "map"}
|
||||
|
||||
if "shape" in dic["key"]:
|
||||
shape = dic["key"]["shape"]
|
||||
@ -958,12 +975,12 @@ class AWSServiceSpec(object):
|
||||
return expand(shape)
|
||||
|
||||
|
||||
def to_str(value, spec):
|
||||
def to_str(value: Any, spec: Dict[str, Any]) -> str:
|
||||
vtype = spec["type"]
|
||||
if vtype == "boolean":
|
||||
return "true" if value else "false"
|
||||
elif vtype == "long":
|
||||
return int(value)
|
||||
return int(value) # type: ignore[return-value]
|
||||
elif vtype == "integer":
|
||||
return str(value)
|
||||
elif vtype == "float":
|
||||
@ -984,7 +1001,7 @@ def to_str(value, spec):
|
||||
raise TypeError("Unknown type {}".format(vtype))
|
||||
|
||||
|
||||
def from_str(value, spec):
|
||||
def from_str(value: str, spec: Dict[str, Any]) -> Any:
|
||||
vtype = spec["type"]
|
||||
if vtype == "boolean":
|
||||
return True if value == "true" else False
|
||||
@ -1001,7 +1018,9 @@ def from_str(value, spec):
|
||||
raise TypeError("Unknown type {}".format(vtype))
|
||||
|
||||
|
||||
def flatten_json_request_body(prefix, dict_body, spec):
|
||||
def flatten_json_request_body(
|
||||
prefix: str, dict_body: Dict[str, Any], spec: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Convert a JSON request body into query params."""
|
||||
if len(spec) == 1 and "type" in spec:
|
||||
return {prefix: to_str(dict_body, spec)}
|
||||
@ -1030,10 +1049,12 @@ def flatten_json_request_body(prefix, dict_body, spec):
|
||||
return dict((prefix + k, v) for k, v in flat.items())
|
||||
|
||||
|
||||
def xml_to_json_response(service_spec, operation, xml, result_node=None):
|
||||
def xml_to_json_response(
|
||||
service_spec: Any, operation: str, xml: str, result_node: Any = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Convert rendered XML response to JSON for use with boto3."""
|
||||
|
||||
def transform(value, spec):
|
||||
def transform(value: Any, spec: Dict[str, Any]) -> Any:
|
||||
"""Apply transformations to make the output JSON comply with the
|
||||
expected form. This function applies:
|
||||
|
||||
@ -1047,7 +1068,7 @@ def xml_to_json_response(service_spec, operation, xml, result_node=None):
|
||||
if len(spec) == 1:
|
||||
return from_str(value, spec)
|
||||
|
||||
od = OrderedDict()
|
||||
od: Dict[str, Any] = OrderedDict()
|
||||
for k, v in value.items():
|
||||
if k.startswith("@"):
|
||||
continue
|
||||
@ -1099,7 +1120,7 @@ def xml_to_json_response(service_spec, operation, xml, result_node=None):
|
||||
for k in result_node or (operation + "Response", operation + "Result"):
|
||||
dic = dic[k]
|
||||
except KeyError:
|
||||
return None
|
||||
return None # type: ignore[return-value]
|
||||
else:
|
||||
return transform(dic, output_spec)
|
||||
return None
|
||||
|
@ -1,5 +1,6 @@
|
||||
# This will only exist in responses >= 0.17
|
||||
import responses
|
||||
from typing import Any, List, Tuple, Optional
|
||||
from .custom_responses_mock import CallbackResponse, not_implemented_callback
|
||||
|
||||
|
||||
@ -10,11 +11,12 @@ class CustomRegistry(responses.registries.FirstMatchRegistry):
|
||||
- CallbackResponses are not discarded after first use - users can mock the same URL as often as they like
|
||||
"""
|
||||
|
||||
def add(self, response):
|
||||
def add(self, response: responses.BaseResponse) -> responses.BaseResponse:
|
||||
if response not in self.registered:
|
||||
super().add(response)
|
||||
return response
|
||||
|
||||
def find(self, request):
|
||||
def find(self, request: Any) -> Tuple[Optional[responses.BaseResponse], List[str]]:
|
||||
all_possibles = responses._default_mock._registry.registered + self.registered
|
||||
found = []
|
||||
match_failed_reasons = []
|
||||
|
@ -2,11 +2,12 @@ import datetime
|
||||
import inspect
|
||||
import re
|
||||
from botocore.exceptions import ClientError
|
||||
from typing import Optional
|
||||
from typing import Any, Optional, List, Callable, Dict
|
||||
from urllib.parse import urlparse
|
||||
from .common_types import TYPE_RESPONSE
|
||||
|
||||
|
||||
def camelcase_to_underscores(argument: Optional[str]) -> str:
|
||||
def camelcase_to_underscores(argument: str) -> str:
|
||||
"""Converts a camelcase param like theNewAttribute to the equivalent
|
||||
python underscore variable like the_new_attribute"""
|
||||
result = ""
|
||||
@ -32,7 +33,7 @@ def camelcase_to_underscores(argument: Optional[str]) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def underscores_to_camelcase(argument):
|
||||
def underscores_to_camelcase(argument: str) -> str:
|
||||
"""Converts a camelcase param like the_new_attribute to the equivalent
|
||||
camelcase version like theNewAttribute. Note that the first letter is
|
||||
NOT capitalized by this function"""
|
||||
@ -48,17 +49,17 @@ def underscores_to_camelcase(argument):
|
||||
return result
|
||||
|
||||
|
||||
def pascal_to_camelcase(argument):
|
||||
def pascal_to_camelcase(argument: str) -> str:
|
||||
"""Converts a PascalCase param to the camelCase equivalent"""
|
||||
return argument[0].lower() + argument[1:]
|
||||
|
||||
|
||||
def camelcase_to_pascal(argument):
|
||||
def camelcase_to_pascal(argument: str) -> str:
|
||||
"""Converts a camelCase param to the PascalCase equivalent"""
|
||||
return argument[0].upper() + argument[1:]
|
||||
|
||||
|
||||
def method_names_from_class(clazz):
|
||||
def method_names_from_class(clazz: object) -> List[str]:
|
||||
predicate = inspect.isfunction
|
||||
return [x[0] for x in inspect.getmembers(clazz, predicate=predicate)]
|
||||
|
||||
@ -70,7 +71,7 @@ def convert_regex_to_flask_path(url_path: str) -> str:
|
||||
for token in ["$"]:
|
||||
url_path = url_path.replace(token, "")
|
||||
|
||||
def caller(reg):
|
||||
def caller(reg: Any) -> str:
|
||||
match_name, match_pattern = reg.groups()
|
||||
return '<regex("{0}"):{1}>'.format(match_pattern, match_name)
|
||||
|
||||
@ -83,11 +84,11 @@ def convert_regex_to_flask_path(url_path: str) -> str:
|
||||
|
||||
|
||||
class convert_to_flask_response(object):
|
||||
def __init__(self, callback):
|
||||
def __init__(self, callback: Callable[..., Any]):
|
||||
self.callback = callback
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
def __name__(self) -> str:
|
||||
# For instance methods, use class and method names. Otherwise
|
||||
# use module and method name
|
||||
if inspect.ismethod(self.callback):
|
||||
@ -96,7 +97,7 @@ class convert_to_flask_response(object):
|
||||
outer = self.callback.__module__
|
||||
return "{0}.{1}".format(outer, self.callback.__name__)
|
||||
|
||||
def __call__(self, args=None, **kwargs):
|
||||
def __call__(self, args: Any = None, **kwargs: Any) -> Any:
|
||||
from flask import request, Response
|
||||
from moto.moto_api import recorder
|
||||
|
||||
@ -118,11 +119,11 @@ class convert_to_flask_response(object):
|
||||
|
||||
|
||||
class convert_flask_to_responses_response(object):
|
||||
def __init__(self, callback):
|
||||
def __init__(self, callback: Callable[..., Any]):
|
||||
self.callback = callback
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
def __name__(self) -> str:
|
||||
# For instance methods, use class and method names. Otherwise
|
||||
# use module and method name
|
||||
if inspect.ismethod(self.callback):
|
||||
@ -131,7 +132,7 @@ class convert_flask_to_responses_response(object):
|
||||
outer = self.callback.__module__
|
||||
return "{0}.{1}".format(outer, self.callback.__name__)
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
def __call__(self, request: Any, *args: Any, **kwargs: Any) -> TYPE_RESPONSE:
|
||||
for key, val in request.headers.items():
|
||||
if isinstance(val, bytes):
|
||||
request.headers[key] = val.decode("utf-8")
|
||||
@ -141,7 +142,7 @@ class convert_flask_to_responses_response(object):
|
||||
return status, headers, response
|
||||
|
||||
|
||||
def iso_8601_datetime_with_milliseconds(value: datetime) -> str:
|
||||
def iso_8601_datetime_with_milliseconds(value: datetime.datetime) -> str:
|
||||
return value.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||
|
||||
|
||||
@ -163,22 +164,22 @@ def iso_8601_datetime_without_milliseconds_s3(
|
||||
RFC1123 = "%a, %d %b %Y %H:%M:%S GMT"
|
||||
|
||||
|
||||
def rfc_1123_datetime(src):
|
||||
def rfc_1123_datetime(src: datetime.datetime) -> str:
|
||||
return src.strftime(RFC1123)
|
||||
|
||||
|
||||
def str_to_rfc_1123_datetime(value):
|
||||
def str_to_rfc_1123_datetime(value: str) -> datetime.datetime:
|
||||
return datetime.datetime.strptime(value, RFC1123)
|
||||
|
||||
|
||||
def unix_time(dt: datetime.datetime = None) -> int:
|
||||
def unix_time(dt: Optional[datetime.datetime] = None) -> float:
|
||||
dt = dt or datetime.datetime.utcnow()
|
||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||
delta = dt - epoch
|
||||
return (delta.days * 86400) + (delta.seconds + (delta.microseconds / 1e6))
|
||||
|
||||
|
||||
def unix_time_millis(dt: datetime = None) -> int:
|
||||
def unix_time_millis(dt: Optional[datetime.datetime] = None) -> float:
|
||||
return unix_time(dt) * 1000.0
|
||||
|
||||
|
||||
@ -193,28 +194,33 @@ def path_url(url: str) -> str:
|
||||
|
||||
|
||||
def tags_from_query_string(
|
||||
querystring_dict, prefix="Tag", key_suffix="Key", value_suffix="Value"
|
||||
):
|
||||
querystring_dict: Dict[str, Any],
|
||||
prefix: str = "Tag",
|
||||
key_suffix: str = "Key",
|
||||
value_suffix: str = "Value",
|
||||
) -> Dict[str, str]:
|
||||
response_values = {}
|
||||
for key in querystring_dict.keys():
|
||||
if key.startswith(prefix) and key.endswith(key_suffix):
|
||||
tag_index = key.replace(prefix + ".", "").replace("." + key_suffix, "")
|
||||
tag_key = querystring_dict.get(
|
||||
tag_key = querystring_dict[
|
||||
"{prefix}.{index}.{key_suffix}".format(
|
||||
prefix=prefix, index=tag_index, key_suffix=key_suffix
|
||||
)
|
||||
)[0]
|
||||
][0]
|
||||
tag_value_key = "{prefix}.{index}.{value_suffix}".format(
|
||||
prefix=prefix, index=tag_index, value_suffix=value_suffix
|
||||
)
|
||||
if tag_value_key in querystring_dict:
|
||||
response_values[tag_key] = querystring_dict.get(tag_value_key)[0]
|
||||
response_values[tag_key] = querystring_dict[tag_value_key][0]
|
||||
else:
|
||||
response_values[tag_key] = None
|
||||
return response_values
|
||||
|
||||
|
||||
def tags_from_cloudformation_tags_list(tags_list):
|
||||
def tags_from_cloudformation_tags_list(
|
||||
tags_list: List[Dict[str, str]]
|
||||
) -> Dict[str, str]:
|
||||
"""Return tags in dict form from cloudformation resource tags form (list of dicts)"""
|
||||
tags = {}
|
||||
for entry in tags_list:
|
||||
@ -225,7 +231,7 @@ def tags_from_cloudformation_tags_list(tags_list):
|
||||
return tags
|
||||
|
||||
|
||||
def remap_nested_keys(root, key_transform):
|
||||
def remap_nested_keys(root: Any, key_transform: Callable[[str], str]) -> Any:
|
||||
"""This remap ("recursive map") function is used to traverse and
|
||||
transform the dictionary keys of arbitrarily nested structures.
|
||||
List comprehensions do not recurse, making it tedious to apply
|
||||
@ -252,7 +258,9 @@ def remap_nested_keys(root, key_transform):
|
||||
return root
|
||||
|
||||
|
||||
def merge_dicts(dict1, dict2, remove_nulls=False):
|
||||
def merge_dicts(
|
||||
dict1: Dict[str, Any], dict2: Dict[str, Any], remove_nulls: bool = False
|
||||
) -> None:
|
||||
"""Given two arbitrarily nested dictionaries, merge the second dict into the first.
|
||||
|
||||
:param dict dict1: the dictionary to be updated.
|
||||
@ -275,7 +283,7 @@ def merge_dicts(dict1, dict2, remove_nulls=False):
|
||||
dict1.pop(key)
|
||||
|
||||
|
||||
def aws_api_matches(pattern, string):
|
||||
def aws_api_matches(pattern: str, string: str) -> bool:
|
||||
"""
|
||||
AWS API can match a value based on a glob, or an exact match
|
||||
"""
|
||||
@ -296,7 +304,7 @@ def aws_api_matches(pattern, string):
|
||||
return False
|
||||
|
||||
|
||||
def extract_region_from_aws_authorization(string):
|
||||
def extract_region_from_aws_authorization(string: str) -> Optional[str]:
|
||||
auth = string or ""
|
||||
region = re.sub(r".*Credential=[^/]+/[^/]+/([^/]+)/.*", r"\1", auth)
|
||||
if region == auth:
|
||||
|
@ -18,7 +18,7 @@ disable = W,C,R,E
|
||||
enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
|
||||
|
||||
[mypy]
|
||||
files= moto/a*,moto/b*,moto/ce,moto/cloudformation,moto/cloudfront,moto/cloudtrail,moto/codebuild,moto/cloudwatch,moto/codepipeline,moto/codecommit,moto/cognito*,moto/comprehend,moto/config,moto/core/base_backend.py
|
||||
files= moto/a*,moto/b*,moto/c*
|
||||
show_column_numbers=True
|
||||
show_error_codes = True
|
||||
disable_error_code=abstract
|
||||
|
Loading…
Reference in New Issue
Block a user