modular_sdk/commons/__init__.py (185 lines of code) (raw):

import base64 import copy import dataclasses import gzip import json import warnings from functools import partial from uuid import uuid4 from boto3.dynamodb.types import TypeDeserializer, TypeSerializer from modular_sdk.commons.exception import ModularException from modular_sdk.commons.log_helper import get_logger _LOG = get_logger(__name__) RESPONSE_BAD_REQUEST_CODE = 400 RESPONSE_UNAUTHORIZED = 401 RESPONSE_FORBIDDEN_CODE = 403 RESPONSE_RESOURCE_NOT_FOUND_CODE = 404 RESPONSE_CONFLICT_CODE = 409 RESPONSE_OK_CODE = 200 RESPONSE_INTERNAL_SERVER_ERROR = 500 RESPONSE_NOT_IMPLEMENTED = 501 RESPONSE_SERVICE_UNAVAILABLE_CODE = 503 def deprecated(message): def deprecated_decorator(func): def deprecated_func(*args, **kwargs): warnings.warn( "{} is a deprecated function. {}".format(func.__name__, message), category=DeprecationWarning, stacklevel=2) warnings.simplefilter('default', DeprecationWarning) return func(*args, **kwargs) return deprecated_func return deprecated_decorator # todo remove with major release @deprecated('not a part of the lib') def build_response(content, code=200): if code == RESPONSE_OK_CODE: if isinstance(content, str): return { 'code': code, 'body': { 'message': content } } elif isinstance(content, dict): return { 'code': code, 'body': { 'items': [content] } } return { 'code': code, 'body': { 'items': content } } raise ModularException( code=code, content=content ) def get_missing_parameters(event, required_params_list): missing_params_list = [] for param in required_params_list: if event.get(param) is None: missing_params_list.append(param) return missing_params_list def validate_params(event, required_params_list): """ Checks if all required parameters present in lambda payload. :param event: the lambda payload :param required_params_list: list of the lambda required parameters :return: bad request response if some parameter[s] is/are missing, otherwise - none """ missing_params_list = get_missing_parameters(event, required_params_list) if missing_params_list: raise ValueError('The following parameters ' 'are missing: {0}'.format(missing_params_list)) def generate_id(): return str(uuid4()) def generate_id_hex(): return str(uuid4().hex) def build_secure_message( request_id: str, command_name: str, parameters_to_secure: dict, secure_parameters: list[str] | None = None, is_flat_request: bool = False, ) -> list[dict] | str: if not secure_parameters: secure_parameters = [] secured_parameters = { k: (v if k not in secure_parameters else '*****') for k, v in parameters_to_secure.items() } return build_message( request_id=request_id, command_name=command_name, parameters=secured_parameters, is_flat_request=is_flat_request, ) def build_message( request_id: str, command_name: str, parameters: list[dict] | dict, is_flat_request: bool = False, compressed: bool = False, ) -> list[dict] | str: if isinstance(parameters, list): result = [] for payload in parameters: result.extend( build_payload(request_id, command_name, payload, is_flat_request) ) else: result = \ build_payload(request_id, command_name, parameters, is_flat_request) if compressed: return base64 \ .b64encode(gzip.compress(json.dumps(result, separators=(',', ':')).encode('UTF-8'))).decode() return result def build_payload( request_id: str, command_name: str, parameters: dict, is_flat_request: bool, ) -> list[dict]: if is_flat_request: parameters.update({'type': command_name}) result = [{'id': request_id, 'type': None, 'params': parameters}] else: result = [{'id': request_id, 'type': command_name,'params': parameters}] return result def default_instance(value, _type: type, *args, **kwargs): return value if isinstance(value, _type) else _type(*args, **kwargs) class SingletonMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class DynamoDBJsonSerializer(SingletonMeta): serializer = TypeSerializer() deserializer = TypeDeserializer() @classmethod def serialize_model(cls, model: dict) -> dict: return { k: cls.serializer.serialize(v) for k, v in model.items() } @classmethod def deserialize_model(cls, model: dict) -> dict: return { k: cls.deserializer.deserialize(v) for k, v in model.items() } def deep_pop(dct: dict, to_pop: dict) -> None: for key, _to_pop in to_pop.items(): if not isinstance(_to_pop, (dict, list)): dct.pop(key, None) continue # isinstance(_to_pop, (dict, list)) _dct = dct.get(key) if type(_dct) != type(_to_pop): continue if isinstance(_to_pop, dict): # going deeper deep_pop(_dct, _to_pop) else: # isinstance(_to_pop, list) for i, d in enumerate(_dct): p = _to_pop[i] if len(to_pop) > i else None if p: deep_pop(d, p) def dict_without(dct: dict, without: dict) -> dict: cp = copy.deepcopy(dct) deep_pop(cp, without) return cp class DataclassBase: """ Provides some useful methods for dataclass instances. Ignore warnings below """ @staticmethod def _factory(x: dict, exclude: set = None) -> dict: dct = {k: v for k, v in x if v is not None} if exclude: [dct.pop(key, None) for key in exclude] return dct def dict(self, exclude: set = None) -> dict: return dataclasses.asdict( self, dict_factory=partial(self._factory, exclude=exclude) ) @classmethod def from_dict(cls, dct: dict): """ Ignoring extra kwargs :param dct: :return: """ return cls(**{ k.name: dct.get(k.name) for k in dataclasses.fields(cls) })