bedrock.endpoints.endpoint

  1import re
  2import json
  3
  4from bedrock._helpers.endpoint import filter_body
  5from bedrock._helpers.list import remove_empties
  6from bedrock._helpers.multi_object_converter import gather_objects_for_saving
  7from bedrock._helpers.string import snake_case_to_camelCase, camelCase_to_snake_case
  8from bedrock.endpoints.dto.bedrock_response import BedrockResponse, BedrockEnvelopedResponse, BedrockDeletionResponse, \
  9    PaginationMetadata
 10from bedrock.exceptions.bedrock_exception import BedrockException
 11from bedrock.exceptions.not_found_exception import NotFoundException
 12from bedrock.exceptions.not_implemented_exception import NotImplementedException
 13from bedrock.log import log_config
 14
 15log = log_config("Endpoint")
 16
 17
 18class Endpoint(object):
 19    """
 20    Creating a class inside your `endpoints` folder that inherits from Endpoint will generate a new endpoint.
 21
 22    Typically, an endpoint will serve both "global" (`/resource/`) and "single" (`/resource/{uuid}`) requests.
 23
 24    To implement the endpoint behaviour, you must override:
 25    - `get_global` for `GET` requests to `/resource/`
 26    - `get_single` for `GET` requests to `/resource/{uuid}`
 27    - `post_global` for `POST` requests to `/resource/`
 28    - `post_single` for `POST` requests to `/resource/{uuid}`
 29    - `put_global` for `PUT` requests to `/resource/`
 30    - `put_single` for `PUT` requests to `/resource/{uuid}`
 31    - `delete_global` for `DELETE` requests to `/resource/`
 32    - `delete_single` for `DELETE` requests to `/resource/{uuid}`
 33
 34    All the above are optional, but you must implement at least one of them.
 35    These methods must return a tuple of `(status_code, body)`, where body is a dict.
 36
 37    Error handling is done automatically when an exception is raised.
 38    """
 39
 40    DEFAULT_ENDPOINTS_DATA = {
 41        "default": {  # The HTTP method. In __custom_endpoints_data__ this should be replaced with GET/PUT/POST/DELETE
 42            "integration_type": "aws_proxy",  # The integration type between the APIGW and the Lambda function
 43            "response_content_type": "application/json",
 44            # The content type expected by the requester. Only use when integration_type is not aws_proxy
 45            "request_content_type": "application/json",
 46            # The content type expected by the APIGW. Only use when integration_type is not aws_proxy
 47            "response_mapping_template": "",
 48            # The template that maps outgoing data from JSON to the content type expected by the requester. Only use when integration_type is not aws_proxy
 49            "request_mapping_template": "",
 50            # The template that maps incoming data to JSON for the lambda function. Only use when integration_type is not aws_proxy
 51            "response_mapping_regex": "default"
 52            # The rule that matches the outgoing lambda HTTP response to the outgoing APIGW response
 53        }
 54    }
 55
 56    __custom_endpoints_data__ = {
 57        "GET": {},
 58        "PUT": {},
 59        "POST": {},
 60        "DELETE": {}
 61    }  #: Override the attributes for each HTTP method's APIGW definition. Should mimic the DEFAULT_ENDPOINTS_DATA dict, instead of 'default' the key is GET/PUT/POST/DELETE
 62
 63    __deprecated__ = False  #: Flag this endpoint as deprecated
 64    __body_schema_name__ = None  #: Override the name of the schema to use for the body
 65    __return_schema_name__ = None  #: Override the name of the schema to use for the response
 66
 67    def __init__(self, resource: str, prefix: str = "/", param_key: str = None, related_model=None):
 68        """
 69        :param resource: The name of the resource. E.g `"/cities/"`
 70        :param prefix: The prefix of the resource (useful when your resource is nested). E.g. `"/countries/{countryUuid}/regions/{regionUuid}/"`. If not provided it will be `"/"`.
 71        :param related_model: The model class that this endpoint is related to. E.g. `City`. If not provided, it will be inferred from the class name.
 72        :param param_key: The name of the path parameter. E.g. `"cityUuid"`. If not provided, it will be inferred from the related model.
 73        """
 74        self.related_model_name = related_model.__name__ if related_model else self.__class__.__name__
 75        self.param_key = param_key if param_key else f"{snake_case_to_camelCase(self.related_model_name)}Uuid"
 76        self.param = f"{{{self.param_key}}}"
 77        self.resource = resource
 78        self.prefix = prefix
 79        self.parent_resources = remove_empties(self.prefix.split("/"))
 80        self.path_params = [r.replace("{", "").replace("}", "") for r in self.parent_resources if r.startswith("{")]
 81        self.global_endpoint = re.sub('/$', '', re.sub('/+', '/', self.prefix + self.resource))
 82        self.single_endpoint = f"{self.global_endpoint}/{self.param}"
 83        self.related_model = related_model
 84
 85    def _get_body_schema_name(self):  # pragma: unit
 86        if self.__body_schema_name__:
 87            return self.__body_schema_name__
 88        if self.related_model is not None:
 89            return self.related_model.__name__
 90        return None
 91
 92    def _get_return_schema_name(self):  # pragma: unit
 93        if self.__return_schema_name__:
 94            return self.__return_schema_name__
 95        if self.related_model is not None:
 96            return self.related_model.__name__
 97        return None
 98
 99    def _get_request_content_type(self, http_method):  # pragma: unit
100        return self._get_endpoint_data(http_method, "request_content_type")
101
102    def _get_integration_type(self, http_method):  # pragma: unit
103        return self._get_endpoint_data(http_method, "integration_type")
104
105    def _get_request_mapping_template(self, http_method):  # pragma: unit
106        return self._get_endpoint_data(http_method, "request_mapping_template")
107
108    def _get_response_mapping_template(self, http_method):  # pragma: unit
109        return self._get_endpoint_data(http_method, "response_mapping_template")
110
111    def _get_response_content_type__(self, http_method):  # pragma: unit
112        return self._get_endpoint_data(http_method, "response_content_type")
113
114    def _get_response_mapping_regex__(self, http_method):  # pragma: unit
115        return self._get_endpoint_data(http_method, "response_mapping_regex")
116
117    def _get_endpoint_data(self, http_method, attribute):  # pragma: unit
118        try:
119            return self.__custom_endpoints_data__[http_method][attribute]
120        except KeyError:
121            log.debug(f"No explicit definition for {http_method}/{attribute}, trying default...")
122        try:
123            return self.DEFAULT_ENDPOINTS_DATA["default"][attribute]
124        except KeyError:
125            log.error(f"There should be a default attribute defined for {attribute} but there isn't one")
126            return None
127
128    @classmethod
129    def global_is_array(cls) -> bool:  # pragma: unit
130        """
131        For OpenAPI spec generation.
132
133        Defaults to returning `True`.
134
135        Override this method to return `False` if the global endpoint should not return an array of items.
136
137        :return: `True` if the global endpoint should return an array of items.
138        """
139        return True
140
141    @classmethod
142    def has_custom_body_schema(cls) -> bool:  # pragma: no cover
143        """
144        For OpenAPI spec generation.
145
146        Defaults to returning `False`.
147
148        Override this method to return `True` if the expected schema of a request body is not derivable from the
149        endpoint's related model (see `__init__`).
150
151        :return: False when the expected schema of a request body is derivable from the endpoint's related model.
152        """
153        return False
154
155    @classmethod
156    def has_custom_return_schema(cls) -> bool:  # pragma: no cover
157        """
158        For OpenAPI spec generation.
159
160        Defaults to returning `False`.
161
162        Override this method to return `True` if the returned object schema is not derivable from the endpoint's
163        related model (see `__init__`).
164
165        :return: False when the returned object schema is derivable from the endpoint's related model.
166        """
167        return False
168
169    @classmethod
170    def get_body_schema(cls) -> str | dict:  # pragma: no cover
171        """
172        For OpenAPI spec generation.
173
174        Defaults to returning `""` and instead derives the schema from the endpoint's related model (see `__init__`).
175
176        Override this method to return a yaml schema of what this endpoint expects in the request body.
177
178        :return: A string of a yaml schema.
179        """
180        return ""
181
182    @classmethod
183    def get_return_schema(cls) -> str | dict:  # pragma: no cover
184        """
185        For OpenAPI spec generation.
186
187        Defaults to returning `""` and instead derives the schema from the endpoint's related model (see `__init__`).
188
189        Override this method to return a yaml schema of the object returned by this endpoint.
190
191        :return: A string of a yaml schema.
192        """
193        return ""
194
195    def _handle(self, event):  # pragma: unit
196        """
197        This is here for backwards compatibility if an endpoint is overriding this...
198        Which they shouldn't since this method is meant to be private >:|
199        But... it has happened >:|
200        """
201        return self._handle_with_context(event)
202
203    def _handle_with_context(self, event, context=None):  # pragma: unit
204        """
205        Kafka and SQS events are of different formats hence why event source is accessed at different levels.
206        See SQS event here: https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#example-standard-queue-message-event
207        """
208        if "wakeup" in event:
209            log.debug("Preventing cold start...")
210            return self.handle_wakeup(event, context)
211
212        if 'eventSource' in event and event['eventSource'] == 'aws:kafka':
213            log.debug("Received Kafka event")
214            try:
215                return self.handle_msk_event(event)
216            except Exception as e:  # pragma: no cover - Just in case
217                log.error(f"Unable to handle Kafka event: {e}")
218                raise e
219        elif ('Records' in event and event['Records'][0].get('eventSource') == 'aws:sqs'
220              and json.loads(event['Records'][0].get('body', '{}')).get('sourceType') == 'kafka-forwarded'):
221            log.debug("Received Kafka forwarded SQS event")
222            try:
223                record = event['Records'][0]
224                kafka_event = json.loads(record['body'])
225                return self.handle_msk_event(kafka_event)
226            except Exception as e:  # pragma: no cover - Just in case
227                log.error(f"Unable to handle SQS event: {e}")
228                raise e
229        else:
230            log.debug("Received APIGW event")
231            return self._handle_http(event, context)
232
233    def handle_wakeup(self, event, context=None):  # pragma: unit
234        return 200, {"status": "OK"}
235
236    def _handle_http(self, event, context=None):  # pragma: unit
237        http_method = event['httpMethod']
238        if http_method == "GET":
239            return self._get(event)
240        if http_method == "POST":
241            return self._post(event)
242        if http_method == "PUT":
243            return self._put(event)
244        if http_method == "DELETE":
245            return self._delete(event)
246        if http_method == "OPTIONS":
247            return self._options(event)
248        raise NotImplementedException()
249
250    def handle_msk_event(self, event) -> tuple[int, dict]:  # pragma: no cover
251        """
252        Override this method to handle the MSK events going to lambda.
253
254        :param event: An event from AWS MSK.
255        :return: A tuple with the status code and the body.
256        """
257        raise NotImplementedException()
258
259    # Get ####
260    def get_global(self, event) -> tuple[int, dict]:  # pragma: unit
261        """
262        Override this method to handle `GET` requests to `/resource/`.
263
264        Usually this should return a list of resources.
265
266        :param event: An event from API Gateway.
267        :return: A tuple with the status code and the body.
268        """
269        raise NotImplementedException()
270
271    def get_single(self, event) -> tuple[int, dict]:  # pragma: unit
272        """
273        Override this method to handle `GET` requests to `/resource/{uuid}`.
274
275        Usually this should return one resource.
276
277        :param event: An event from API Gateway.
278        :return: A tuple with the status code and the body.
279        """
280        raise NotImplementedException()
281
282    def get_global_generic(self,
283                           event,
284                           model_cls=None,
285                           belongs_to: dict = None,
286                           allow_listing_all: bool = False,
287                           order_by=None, order='asc',
288                           offset=None, limit=None,
289                           load_options=None) -> tuple[int, BedrockResponse]:  # pragma: integration
290        """
291        Use this method to automatically handle getting a list of `model_cls` resources.
292
293        :param event: An event from API Gateway.
294        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
295        :param belongs_to: A dictionary of key-value pairs that the items must match.
296        :param allow_listing_all: Whether to allow listing all items. Defaults to `False`.
297        :param order_by: The column to sort results of the query on, if any.
298        :param order: The order to sort results of the query in.
299        :param offset: The offset to use for pagination of results, if any.
300        :param limit: The number of rows to return in the results, if any.
301        :param load_options: The options to use for loading the results, if any.
302        :return: A tuple with the status code and a list of resources.
303        """
304        _model_cls = model_cls if model_cls else self.related_model
305        log.debug(f"Getting all {_model_cls.__name__} items (load_options: {load_options})")
306        try:
307            items, count = _model_cls.find_all_matching_criteria(belongs_to,
308                                                                 allow_listing_all=allow_listing_all,
309                                                                 order_by=order_by, order=order,
310                                                                 offset=offset, limit=limit,
311                                                                 load_options=load_options,
312                                                                 with_count=True)
313        except Exception as e:
314            raise NotFoundException(f'Unable to find {_model_cls.__name__} list') from e
315
316        response = [item.as_json() for item in items]
317        return 200, BedrockEnvelopedResponse(
318            response,
319            event,
320            PaginationMetadata(order_by, order, limit, offset, count)
321        )
322
323    def get_single_generic(self,
324                           event,
325                           model_cls=None,
326                           belongs_to: dict = None,
327                           load_options=None,
328                           resource_id: str | dict = None) -> tuple[int, BedrockResponse]:  # pragma: integration
329        """
330        Use this method to automatically handle getting one `model_cls` resource.
331
332        :param event: An event from API Gateway.
333        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
334        :param belongs_to: A dictionary of key-value pairs that the items must match.
335        :param load_options: The options to use for loading the results, if any.
336        :param resource_id: Resource id that can be optionally passed in
337        :return: A tuple with the status code `200` and one resource.
338        """
339        _model_cls = model_cls if model_cls else self.related_model
340        if not resource_id:
341            try:
342                resource_id = event["pathParameters"][self.param_key]
343            except Exception as e:
344                raise NotFoundException(f"{self.param} not provided in url") from e
345
346        log.debug(f"Getting {_model_cls.__name__}: {resource_id}")
347        try:
348            response = _model_cls.get(resource_id,
349                                      restrictions=belongs_to,
350                                      load_options=load_options).as_json()
351        except Exception as e:
352            raise NotFoundException(
353                f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}') from e
354
355        return 200, BedrockEnvelopedResponse(response, event)
356
357    def _get(self, event):  # pragma: unit
358        if event.get('resource') == self.single_endpoint:
359            return self.get_single(event)
360        else:
361            return self.get_global(event)
362
363    # Post ####
364    def post_global(self, event) -> tuple[int, dict]:  # pragma: unit
365        """
366        Override this method to handle `POST` requests to `/resource/`.
367
368        Usually this should create a new resource.
369
370        :param event: An event from API Gateway.
371        :return: A tuple with the status code and, typically, the body of what was just created.
372        """
373        raise NotImplementedException()
374
375    def post_single(self, event) -> tuple[int, dict]:  # pragma: unit
376        """
377        Override this method to handle `POST` requests to `/resource/{uuid}`.
378
379        This doesn't usually get any use, but feel free to override it if you need to.
380        It's here for completeness, but is fully functional.
381
382        :param event: An event from API Gateway.
383        :return: A tuple with the status code and the body.
384        """
385        raise NotImplementedException()
386
387    def post_global_generic(self,
388                            event,
389                            model_cls=None,
390                            altered_body: dict = None,
391                            excludes=[],
392                            extra_objects={},
393                            load_options=None,
394                            save_nested=False) -> tuple[int, BedrockEnvelopedResponse]:  # pragma: integration
395        """
396        Use this method to automatically handle creating a new `model_cls` resource.
397        :param event: An event from API Gateway.
398        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
399        :param altered_body: Optionally, override the body provided by the request.
400        :param excludes: A list of keys to exclude from `model_cls` when returning a response.
401        :param extra_objects: A dictionary of key-value pairs to return with the response.
402        :param load_options: The options to use for loading the results on re-fetch, if any.
403        :param save_nested: Whether to update/create nested models provided in the body. Defaults to `False`.
404        :return: A tuple with status code `201` and the body of the created resource.
405        """
406        _model_cls = model_cls if model_cls else self.related_model
407        body = filter_body(event['body'], altered_body)
408        result = self._save_object_helper(_model_cls, body, excludes, extra_objects,
409                                          load_options=load_options, save_nested=save_nested)
410        return 201, BedrockEnvelopedResponse(result, event)
411
412    def _post(self, event):  # pragma: unit
413        if event.get('resource') == self.single_endpoint:
414            return self.post_single(event)
415        else:
416            return self.post_global(event)
417
418    # Put ####
419    def put_global(self, event) -> tuple[int, dict]:  # pragma: unit
420        """
421        Override this method to handle `PUT` requests to `/resource/`.
422
423        This doesn't usually get any use, but feel free to override it if you need to.
424        It's here for completeness, but is fully functional.
425
426        :param event: An event from API Gateway.
427        :return: A tuple with the status code and the body.
428        """
429        raise NotImplementedException()
430
431    def put_single(self, event) -> tuple[int, dict]:  # pragma: unit
432        """
433        Override this method to handle `PUT` requests to `/resource/{uuid}`.
434
435        Usually this should edit an existing resource.
436
437        :param event: An event from API Gateway.
438        :return: A tuple with the status code and, typically, the body of what was just modified.
439        """
440        raise NotImplementedException()
441
442    def put_single_generic(self,
443                           event,
444                           model_cls=None,
445                           unique_column: str | dict = None,
446                           altered_body: dict = None,
447                           belongs_to={},
448                           excludes=[],
449                           extra_objects={},
450                           load_options=None,
451                           save_nested=False) -> tuple[int, BedrockEnvelopedResponse]:  # pragma: integration
452        """
453        Use this method to automatically handle editing an existing `model_cls` resource.
454        :param event: An event from API Gateway.
455        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
456        :param unique_column: The unique column for the `model_cls` (can be a dictionary if resource has a composite primary key). Defaults to `model_cls.get_unique_identifier_names()`.
457        :param altered_body: Optionally, override the body provided by the request.
458        :param belongs_to: A dictionary that restricts the model being edited by any parent resources it should belong to.
459        :param excludes: A list of keys to exclude from `model_cls` when returning a response.
460        :param extra_objects: A dictionary of key-value pairs to return with the response.
461        :param load_options: The options to use for loading the results, if any.
462        :param save_nested: Whether to update/create nested models provided in the body. Defaults to `False`.
463        :return: A tuple with status code `200` and the body of the edited resource.
464        """
465        _model_cls = model_cls if model_cls else self.related_model
466        body = filter_body(event['body'], altered_body)
467        objs = _model_cls.find_all_by_unique_column(body,
468                                                    unique_column=unique_column,
469                                                    restrictions=belongs_to,
470                                                    load_options=load_options)
471        if not objs or len(objs) == 0:
472            if _model_cls.should_create_if_not_found():
473                return self.post_global_generic(event, _model_cls, extra_objects=extra_objects, altered_body=body,
474                                                load_options=load_options, save_nested=save_nested)
475            else:
476                raise NotFoundException(f"{_model_cls.__name__} was not found")
477
478        obj = objs[0]
479        original_data = obj.as_json()
480
481        nested_model_keys = [m.key for m in _model_cls.get_nested_models()]
482        for key, value in body.items():
483            snake_key = camelCase_to_snake_case(key)
484            if snake_key not in nested_model_keys:
485                setattr(obj, snake_key, value)
486
487        result = self._save_object_helper(_model_cls, body, excludes, extra_objects, obj,
488                                          load_options=load_options, save_nested=save_nested)
489        return 200, BedrockEnvelopedResponse(result, event, original_data=original_data)
490
491    def _put(self, event):  # pragma: unit
492        if event.get('resource') == self.single_endpoint:
493            return self.put_single(event)
494        else:
495            return self.put_global(event)
496
497    # Delete ####
498    def delete_global(self, event) -> tuple[int, dict]:  # pragma: unit
499        """
500        Override this method to handle `DELETE` requests to `/resource/`.
501
502        This doesn't usually get any use, but feel free to override it if you need to.
503        It's here for completeness, but is fully functional.
504
505        :param event: An event from API Gateway.
506        :return: A tuple with the status code and the body.
507        """
508        raise NotImplementedException()
509
510    def delete_single(self, event) -> tuple[int, dict]:  # pragma: unit
511        """
512        Override this method to handle `DELETE` requests to `/resource/{uuid}`.
513
514        Usually this should delete an existing resource.
515
516        :param event: An event from API Gateway.
517        :return: A tuple with the status code and, typically, the body of what was just deleted.
518        """
519        raise NotImplementedException()
520
521    def delete_single_generic(self,
522                              event,
523                              model_cls=None,
524                              resource_id: str | dict = None,
525                              belongs_to: dict = None,
526                              load_options=None) -> tuple[int, BedrockDeletionResponse]:  # pragma: integration
527        """
528        Use this method to automatically handle deleting an existing `model_cls` resource.
529
530        :param event: An event from API Gateway.
531        :param model_cls: The model class that you want to manipulate.
532        :param resource_id: The unique identifier of the resource to delete (can be a dictionary if resource has a composite primary key). Will throw an exception if not provided.
533        :param belongs_to: A dictionary that restricts the model being deleted by any parent resources it should belong to.
534        :param load_options: The options to use for loading the results, if any.
535        :return: A tuple with status code `202` and a body confirming the deletion accompanied by the original object.
536        """
537        _model_cls = model_cls if model_cls else self.related_model
538        if not resource_id:
539            raise BedrockException("Missing resource_id argument - if you're seeing this, the developer screwed up!")
540
541        log.debug(f"Deleting {_model_cls.__name__}: {resource_id}")
542        try:
543            obj = _model_cls.get(resource_id, restrictions=belongs_to, load_options=load_options)
544        except Exception as e:
545            raise NotFoundException(
546                f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}') from e
547        if obj is None:
548            raise NotFoundException(f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}')
549        json_obj = obj.as_json()
550        obj.delete()
551        return 202, BedrockDeletionResponse(json_obj, event)
552
553    def _delete(self, event):  # pragma: unit
554        if event.get('resource') == self.single_endpoint:
555            return self.delete_single(event)
556        else:
557            return self.delete_global(event)
558
559    def _options(self, event):  # pragma: unit
560        return 200, {}
561
562    def _save_object_helper(self,
563                            cls,
564                            body,
565                            excludes,
566                            extra_objects,
567                            model_obj=None,
568                            load_options=None,
569                            save_nested=False):  # pragma: integration
570        objects = gather_objects_for_saving(cls,
571                                            body,
572                                            auto_populate=not model_obj,
573                                            save_nested=save_nested)
574        main_obj = model_obj if model_obj else objects[next(iter(objects))][0]
575        main_obj.save()
576        for model_name in list(objects.keys())[1:]:
577            models = objects[model_name]
578            models[0].__class__.save_many(models, duplicates_strategy="on_conflict_do_update", preserve_uuids=True)
579        refetched_obj = main_obj.refetch(load_options=load_options)
580        if not refetched_obj:
581            raise BedrockException(
582                "Unable to find object after saving it. This shouldn't happen... but if it does, please report it!")
583        return refetched_obj.as_custom_json(**extra_objects) \
584            if refetched_obj.__class__.has_custom_json() else refetched_obj.as_json(excludes=excludes,
585                                                                                    extra=extra_objects)
log = <MyLogger BEDROCK-Endpoint (INFO)>
class Endpoint:
 19class Endpoint(object):
 20    """
 21    Creating a class inside your `endpoints` folder that inherits from Endpoint will generate a new endpoint.
 22
 23    Typically, an endpoint will serve both "global" (`/resource/`) and "single" (`/resource/{uuid}`) requests.
 24
 25    To implement the endpoint behaviour, you must override:
 26    - `get_global` for `GET` requests to `/resource/`
 27    - `get_single` for `GET` requests to `/resource/{uuid}`
 28    - `post_global` for `POST` requests to `/resource/`
 29    - `post_single` for `POST` requests to `/resource/{uuid}`
 30    - `put_global` for `PUT` requests to `/resource/`
 31    - `put_single` for `PUT` requests to `/resource/{uuid}`
 32    - `delete_global` for `DELETE` requests to `/resource/`
 33    - `delete_single` for `DELETE` requests to `/resource/{uuid}`
 34
 35    All the above are optional, but you must implement at least one of them.
 36    These methods must return a tuple of `(status_code, body)`, where body is a dict.
 37
 38    Error handling is done automatically when an exception is raised.
 39    """
 40
 41    DEFAULT_ENDPOINTS_DATA = {
 42        "default": {  # The HTTP method. In __custom_endpoints_data__ this should be replaced with GET/PUT/POST/DELETE
 43            "integration_type": "aws_proxy",  # The integration type between the APIGW and the Lambda function
 44            "response_content_type": "application/json",
 45            # The content type expected by the requester. Only use when integration_type is not aws_proxy
 46            "request_content_type": "application/json",
 47            # The content type expected by the APIGW. Only use when integration_type is not aws_proxy
 48            "response_mapping_template": "",
 49            # The template that maps outgoing data from JSON to the content type expected by the requester. Only use when integration_type is not aws_proxy
 50            "request_mapping_template": "",
 51            # The template that maps incoming data to JSON for the lambda function. Only use when integration_type is not aws_proxy
 52            "response_mapping_regex": "default"
 53            # The rule that matches the outgoing lambda HTTP response to the outgoing APIGW response
 54        }
 55    }
 56
 57    __custom_endpoints_data__ = {
 58        "GET": {},
 59        "PUT": {},
 60        "POST": {},
 61        "DELETE": {}
 62    }  #: Override the attributes for each HTTP method's APIGW definition. Should mimic the DEFAULT_ENDPOINTS_DATA dict, instead of 'default' the key is GET/PUT/POST/DELETE
 63
 64    __deprecated__ = False  #: Flag this endpoint as deprecated
 65    __body_schema_name__ = None  #: Override the name of the schema to use for the body
 66    __return_schema_name__ = None  #: Override the name of the schema to use for the response
 67
 68    def __init__(self, resource: str, prefix: str = "/", param_key: str = None, related_model=None):
 69        """
 70        :param resource: The name of the resource. E.g `"/cities/"`
 71        :param prefix: The prefix of the resource (useful when your resource is nested). E.g. `"/countries/{countryUuid}/regions/{regionUuid}/"`. If not provided it will be `"/"`.
 72        :param related_model: The model class that this endpoint is related to. E.g. `City`. If not provided, it will be inferred from the class name.
 73        :param param_key: The name of the path parameter. E.g. `"cityUuid"`. If not provided, it will be inferred from the related model.
 74        """
 75        self.related_model_name = related_model.__name__ if related_model else self.__class__.__name__
 76        self.param_key = param_key if param_key else f"{snake_case_to_camelCase(self.related_model_name)}Uuid"
 77        self.param = f"{{{self.param_key}}}"
 78        self.resource = resource
 79        self.prefix = prefix
 80        self.parent_resources = remove_empties(self.prefix.split("/"))
 81        self.path_params = [r.replace("{", "").replace("}", "") for r in self.parent_resources if r.startswith("{")]
 82        self.global_endpoint = re.sub('/$', '', re.sub('/+', '/', self.prefix + self.resource))
 83        self.single_endpoint = f"{self.global_endpoint}/{self.param}"
 84        self.related_model = related_model
 85
 86    def _get_body_schema_name(self):  # pragma: unit
 87        if self.__body_schema_name__:
 88            return self.__body_schema_name__
 89        if self.related_model is not None:
 90            return self.related_model.__name__
 91        return None
 92
 93    def _get_return_schema_name(self):  # pragma: unit
 94        if self.__return_schema_name__:
 95            return self.__return_schema_name__
 96        if self.related_model is not None:
 97            return self.related_model.__name__
 98        return None
 99
100    def _get_request_content_type(self, http_method):  # pragma: unit
101        return self._get_endpoint_data(http_method, "request_content_type")
102
103    def _get_integration_type(self, http_method):  # pragma: unit
104        return self._get_endpoint_data(http_method, "integration_type")
105
106    def _get_request_mapping_template(self, http_method):  # pragma: unit
107        return self._get_endpoint_data(http_method, "request_mapping_template")
108
109    def _get_response_mapping_template(self, http_method):  # pragma: unit
110        return self._get_endpoint_data(http_method, "response_mapping_template")
111
112    def _get_response_content_type__(self, http_method):  # pragma: unit
113        return self._get_endpoint_data(http_method, "response_content_type")
114
115    def _get_response_mapping_regex__(self, http_method):  # pragma: unit
116        return self._get_endpoint_data(http_method, "response_mapping_regex")
117
118    def _get_endpoint_data(self, http_method, attribute):  # pragma: unit
119        try:
120            return self.__custom_endpoints_data__[http_method][attribute]
121        except KeyError:
122            log.debug(f"No explicit definition for {http_method}/{attribute}, trying default...")
123        try:
124            return self.DEFAULT_ENDPOINTS_DATA["default"][attribute]
125        except KeyError:
126            log.error(f"There should be a default attribute defined for {attribute} but there isn't one")
127            return None
128
129    @classmethod
130    def global_is_array(cls) -> bool:  # pragma: unit
131        """
132        For OpenAPI spec generation.
133
134        Defaults to returning `True`.
135
136        Override this method to return `False` if the global endpoint should not return an array of items.
137
138        :return: `True` if the global endpoint should return an array of items.
139        """
140        return True
141
142    @classmethod
143    def has_custom_body_schema(cls) -> bool:  # pragma: no cover
144        """
145        For OpenAPI spec generation.
146
147        Defaults to returning `False`.
148
149        Override this method to return `True` if the expected schema of a request body is not derivable from the
150        endpoint's related model (see `__init__`).
151
152        :return: False when the expected schema of a request body is derivable from the endpoint's related model.
153        """
154        return False
155
156    @classmethod
157    def has_custom_return_schema(cls) -> bool:  # pragma: no cover
158        """
159        For OpenAPI spec generation.
160
161        Defaults to returning `False`.
162
163        Override this method to return `True` if the returned object schema is not derivable from the endpoint's
164        related model (see `__init__`).
165
166        :return: False when the returned object schema is derivable from the endpoint's related model.
167        """
168        return False
169
170    @classmethod
171    def get_body_schema(cls) -> str | dict:  # pragma: no cover
172        """
173        For OpenAPI spec generation.
174
175        Defaults to returning `""` and instead derives the schema from the endpoint's related model (see `__init__`).
176
177        Override this method to return a yaml schema of what this endpoint expects in the request body.
178
179        :return: A string of a yaml schema.
180        """
181        return ""
182
183    @classmethod
184    def get_return_schema(cls) -> str | dict:  # pragma: no cover
185        """
186        For OpenAPI spec generation.
187
188        Defaults to returning `""` and instead derives the schema from the endpoint's related model (see `__init__`).
189
190        Override this method to return a yaml schema of the object returned by this endpoint.
191
192        :return: A string of a yaml schema.
193        """
194        return ""
195
196    def _handle(self, event):  # pragma: unit
197        """
198        This is here for backwards compatibility if an endpoint is overriding this...
199        Which they shouldn't since this method is meant to be private >:|
200        But... it has happened >:|
201        """
202        return self._handle_with_context(event)
203
204    def _handle_with_context(self, event, context=None):  # pragma: unit
205        """
206        Kafka and SQS events are of different formats hence why event source is accessed at different levels.
207        See SQS event here: https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#example-standard-queue-message-event
208        """
209        if "wakeup" in event:
210            log.debug("Preventing cold start...")
211            return self.handle_wakeup(event, context)
212
213        if 'eventSource' in event and event['eventSource'] == 'aws:kafka':
214            log.debug("Received Kafka event")
215            try:
216                return self.handle_msk_event(event)
217            except Exception as e:  # pragma: no cover - Just in case
218                log.error(f"Unable to handle Kafka event: {e}")
219                raise e
220        elif ('Records' in event and event['Records'][0].get('eventSource') == 'aws:sqs'
221              and json.loads(event['Records'][0].get('body', '{}')).get('sourceType') == 'kafka-forwarded'):
222            log.debug("Received Kafka forwarded SQS event")
223            try:
224                record = event['Records'][0]
225                kafka_event = json.loads(record['body'])
226                return self.handle_msk_event(kafka_event)
227            except Exception as e:  # pragma: no cover - Just in case
228                log.error(f"Unable to handle SQS event: {e}")
229                raise e
230        else:
231            log.debug("Received APIGW event")
232            return self._handle_http(event, context)
233
234    def handle_wakeup(self, event, context=None):  # pragma: unit
235        return 200, {"status": "OK"}
236
237    def _handle_http(self, event, context=None):  # pragma: unit
238        http_method = event['httpMethod']
239        if http_method == "GET":
240            return self._get(event)
241        if http_method == "POST":
242            return self._post(event)
243        if http_method == "PUT":
244            return self._put(event)
245        if http_method == "DELETE":
246            return self._delete(event)
247        if http_method == "OPTIONS":
248            return self._options(event)
249        raise NotImplementedException()
250
251    def handle_msk_event(self, event) -> tuple[int, dict]:  # pragma: no cover
252        """
253        Override this method to handle the MSK events going to lambda.
254
255        :param event: An event from AWS MSK.
256        :return: A tuple with the status code and the body.
257        """
258        raise NotImplementedException()
259
260    # Get ####
261    def get_global(self, event) -> tuple[int, dict]:  # pragma: unit
262        """
263        Override this method to handle `GET` requests to `/resource/`.
264
265        Usually this should return a list of resources.
266
267        :param event: An event from API Gateway.
268        :return: A tuple with the status code and the body.
269        """
270        raise NotImplementedException()
271
272    def get_single(self, event) -> tuple[int, dict]:  # pragma: unit
273        """
274        Override this method to handle `GET` requests to `/resource/{uuid}`.
275
276        Usually this should return one resource.
277
278        :param event: An event from API Gateway.
279        :return: A tuple with the status code and the body.
280        """
281        raise NotImplementedException()
282
283    def get_global_generic(self,
284                           event,
285                           model_cls=None,
286                           belongs_to: dict = None,
287                           allow_listing_all: bool = False,
288                           order_by=None, order='asc',
289                           offset=None, limit=None,
290                           load_options=None) -> tuple[int, BedrockResponse]:  # pragma: integration
291        """
292        Use this method to automatically handle getting a list of `model_cls` resources.
293
294        :param event: An event from API Gateway.
295        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
296        :param belongs_to: A dictionary of key-value pairs that the items must match.
297        :param allow_listing_all: Whether to allow listing all items. Defaults to `False`.
298        :param order_by: The column to sort results of the query on, if any.
299        :param order: The order to sort results of the query in.
300        :param offset: The offset to use for pagination of results, if any.
301        :param limit: The number of rows to return in the results, if any.
302        :param load_options: The options to use for loading the results, if any.
303        :return: A tuple with the status code and a list of resources.
304        """
305        _model_cls = model_cls if model_cls else self.related_model
306        log.debug(f"Getting all {_model_cls.__name__} items (load_options: {load_options})")
307        try:
308            items, count = _model_cls.find_all_matching_criteria(belongs_to,
309                                                                 allow_listing_all=allow_listing_all,
310                                                                 order_by=order_by, order=order,
311                                                                 offset=offset, limit=limit,
312                                                                 load_options=load_options,
313                                                                 with_count=True)
314        except Exception as e:
315            raise NotFoundException(f'Unable to find {_model_cls.__name__} list') from e
316
317        response = [item.as_json() for item in items]
318        return 200, BedrockEnvelopedResponse(
319            response,
320            event,
321            PaginationMetadata(order_by, order, limit, offset, count)
322        )
323
324    def get_single_generic(self,
325                           event,
326                           model_cls=None,
327                           belongs_to: dict = None,
328                           load_options=None,
329                           resource_id: str | dict = None) -> tuple[int, BedrockResponse]:  # pragma: integration
330        """
331        Use this method to automatically handle getting one `model_cls` resource.
332
333        :param event: An event from API Gateway.
334        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
335        :param belongs_to: A dictionary of key-value pairs that the items must match.
336        :param load_options: The options to use for loading the results, if any.
337        :param resource_id: Resource id that can be optionally passed in
338        :return: A tuple with the status code `200` and one resource.
339        """
340        _model_cls = model_cls if model_cls else self.related_model
341        if not resource_id:
342            try:
343                resource_id = event["pathParameters"][self.param_key]
344            except Exception as e:
345                raise NotFoundException(f"{self.param} not provided in url") from e
346
347        log.debug(f"Getting {_model_cls.__name__}: {resource_id}")
348        try:
349            response = _model_cls.get(resource_id,
350                                      restrictions=belongs_to,
351                                      load_options=load_options).as_json()
352        except Exception as e:
353            raise NotFoundException(
354                f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}') from e
355
356        return 200, BedrockEnvelopedResponse(response, event)
357
358    def _get(self, event):  # pragma: unit
359        if event.get('resource') == self.single_endpoint:
360            return self.get_single(event)
361        else:
362            return self.get_global(event)
363
364    # Post ####
365    def post_global(self, event) -> tuple[int, dict]:  # pragma: unit
366        """
367        Override this method to handle `POST` requests to `/resource/`.
368
369        Usually this should create a new resource.
370
371        :param event: An event from API Gateway.
372        :return: A tuple with the status code and, typically, the body of what was just created.
373        """
374        raise NotImplementedException()
375
376    def post_single(self, event) -> tuple[int, dict]:  # pragma: unit
377        """
378        Override this method to handle `POST` requests to `/resource/{uuid}`.
379
380        This doesn't usually get any use, but feel free to override it if you need to.
381        It's here for completeness, but is fully functional.
382
383        :param event: An event from API Gateway.
384        :return: A tuple with the status code and the body.
385        """
386        raise NotImplementedException()
387
388    def post_global_generic(self,
389                            event,
390                            model_cls=None,
391                            altered_body: dict = None,
392                            excludes=[],
393                            extra_objects={},
394                            load_options=None,
395                            save_nested=False) -> tuple[int, BedrockEnvelopedResponse]:  # pragma: integration
396        """
397        Use this method to automatically handle creating a new `model_cls` resource.
398        :param event: An event from API Gateway.
399        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
400        :param altered_body: Optionally, override the body provided by the request.
401        :param excludes: A list of keys to exclude from `model_cls` when returning a response.
402        :param extra_objects: A dictionary of key-value pairs to return with the response.
403        :param load_options: The options to use for loading the results on re-fetch, if any.
404        :param save_nested: Whether to update/create nested models provided in the body. Defaults to `False`.
405        :return: A tuple with status code `201` and the body of the created resource.
406        """
407        _model_cls = model_cls if model_cls else self.related_model
408        body = filter_body(event['body'], altered_body)
409        result = self._save_object_helper(_model_cls, body, excludes, extra_objects,
410                                          load_options=load_options, save_nested=save_nested)
411        return 201, BedrockEnvelopedResponse(result, event)
412
413    def _post(self, event):  # pragma: unit
414        if event.get('resource') == self.single_endpoint:
415            return self.post_single(event)
416        else:
417            return self.post_global(event)
418
419    # Put ####
420    def put_global(self, event) -> tuple[int, dict]:  # pragma: unit
421        """
422        Override this method to handle `PUT` requests to `/resource/`.
423
424        This doesn't usually get any use, but feel free to override it if you need to.
425        It's here for completeness, but is fully functional.
426
427        :param event: An event from API Gateway.
428        :return: A tuple with the status code and the body.
429        """
430        raise NotImplementedException()
431
432    def put_single(self, event) -> tuple[int, dict]:  # pragma: unit
433        """
434        Override this method to handle `PUT` requests to `/resource/{uuid}`.
435
436        Usually this should edit an existing resource.
437
438        :param event: An event from API Gateway.
439        :return: A tuple with the status code and, typically, the body of what was just modified.
440        """
441        raise NotImplementedException()
442
443    def put_single_generic(self,
444                           event,
445                           model_cls=None,
446                           unique_column: str | dict = None,
447                           altered_body: dict = None,
448                           belongs_to={},
449                           excludes=[],
450                           extra_objects={},
451                           load_options=None,
452                           save_nested=False) -> tuple[int, BedrockEnvelopedResponse]:  # pragma: integration
453        """
454        Use this method to automatically handle editing an existing `model_cls` resource.
455        :param event: An event from API Gateway.
456        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
457        :param unique_column: The unique column for the `model_cls` (can be a dictionary if resource has a composite primary key). Defaults to `model_cls.get_unique_identifier_names()`.
458        :param altered_body: Optionally, override the body provided by the request.
459        :param belongs_to: A dictionary that restricts the model being edited by any parent resources it should belong to.
460        :param excludes: A list of keys to exclude from `model_cls` when returning a response.
461        :param extra_objects: A dictionary of key-value pairs to return with the response.
462        :param load_options: The options to use for loading the results, if any.
463        :param save_nested: Whether to update/create nested models provided in the body. Defaults to `False`.
464        :return: A tuple with status code `200` and the body of the edited resource.
465        """
466        _model_cls = model_cls if model_cls else self.related_model
467        body = filter_body(event['body'], altered_body)
468        objs = _model_cls.find_all_by_unique_column(body,
469                                                    unique_column=unique_column,
470                                                    restrictions=belongs_to,
471                                                    load_options=load_options)
472        if not objs or len(objs) == 0:
473            if _model_cls.should_create_if_not_found():
474                return self.post_global_generic(event, _model_cls, extra_objects=extra_objects, altered_body=body,
475                                                load_options=load_options, save_nested=save_nested)
476            else:
477                raise NotFoundException(f"{_model_cls.__name__} was not found")
478
479        obj = objs[0]
480        original_data = obj.as_json()
481
482        nested_model_keys = [m.key for m in _model_cls.get_nested_models()]
483        for key, value in body.items():
484            snake_key = camelCase_to_snake_case(key)
485            if snake_key not in nested_model_keys:
486                setattr(obj, snake_key, value)
487
488        result = self._save_object_helper(_model_cls, body, excludes, extra_objects, obj,
489                                          load_options=load_options, save_nested=save_nested)
490        return 200, BedrockEnvelopedResponse(result, event, original_data=original_data)
491
492    def _put(self, event):  # pragma: unit
493        if event.get('resource') == self.single_endpoint:
494            return self.put_single(event)
495        else:
496            return self.put_global(event)
497
498    # Delete ####
499    def delete_global(self, event) -> tuple[int, dict]:  # pragma: unit
500        """
501        Override this method to handle `DELETE` requests to `/resource/`.
502
503        This doesn't usually get any use, but feel free to override it if you need to.
504        It's here for completeness, but is fully functional.
505
506        :param event: An event from API Gateway.
507        :return: A tuple with the status code and the body.
508        """
509        raise NotImplementedException()
510
511    def delete_single(self, event) -> tuple[int, dict]:  # pragma: unit
512        """
513        Override this method to handle `DELETE` requests to `/resource/{uuid}`.
514
515        Usually this should delete an existing resource.
516
517        :param event: An event from API Gateway.
518        :return: A tuple with the status code and, typically, the body of what was just deleted.
519        """
520        raise NotImplementedException()
521
522    def delete_single_generic(self,
523                              event,
524                              model_cls=None,
525                              resource_id: str | dict = None,
526                              belongs_to: dict = None,
527                              load_options=None) -> tuple[int, BedrockDeletionResponse]:  # pragma: integration
528        """
529        Use this method to automatically handle deleting an existing `model_cls` resource.
530
531        :param event: An event from API Gateway.
532        :param model_cls: The model class that you want to manipulate.
533        :param resource_id: The unique identifier of the resource to delete (can be a dictionary if resource has a composite primary key). Will throw an exception if not provided.
534        :param belongs_to: A dictionary that restricts the model being deleted by any parent resources it should belong to.
535        :param load_options: The options to use for loading the results, if any.
536        :return: A tuple with status code `202` and a body confirming the deletion accompanied by the original object.
537        """
538        _model_cls = model_cls if model_cls else self.related_model
539        if not resource_id:
540            raise BedrockException("Missing resource_id argument - if you're seeing this, the developer screwed up!")
541
542        log.debug(f"Deleting {_model_cls.__name__}: {resource_id}")
543        try:
544            obj = _model_cls.get(resource_id, restrictions=belongs_to, load_options=load_options)
545        except Exception as e:
546            raise NotFoundException(
547                f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}') from e
548        if obj is None:
549            raise NotFoundException(f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}')
550        json_obj = obj.as_json()
551        obj.delete()
552        return 202, BedrockDeletionResponse(json_obj, event)
553
554    def _delete(self, event):  # pragma: unit
555        if event.get('resource') == self.single_endpoint:
556            return self.delete_single(event)
557        else:
558            return self.delete_global(event)
559
560    def _options(self, event):  # pragma: unit
561        return 200, {}
562
563    def _save_object_helper(self,
564                            cls,
565                            body,
566                            excludes,
567                            extra_objects,
568                            model_obj=None,
569                            load_options=None,
570                            save_nested=False):  # pragma: integration
571        objects = gather_objects_for_saving(cls,
572                                            body,
573                                            auto_populate=not model_obj,
574                                            save_nested=save_nested)
575        main_obj = model_obj if model_obj else objects[next(iter(objects))][0]
576        main_obj.save()
577        for model_name in list(objects.keys())[1:]:
578            models = objects[model_name]
579            models[0].__class__.save_many(models, duplicates_strategy="on_conflict_do_update", preserve_uuids=True)
580        refetched_obj = main_obj.refetch(load_options=load_options)
581        if not refetched_obj:
582            raise BedrockException(
583                "Unable to find object after saving it. This shouldn't happen... but if it does, please report it!")
584        return refetched_obj.as_custom_json(**extra_objects) \
585            if refetched_obj.__class__.has_custom_json() else refetched_obj.as_json(excludes=excludes,
586                                                                                    extra=extra_objects)

Creating a class inside your endpoints folder that inherits from Endpoint will generate a new endpoint.

Typically, an endpoint will serve both "global" (/resource/) and "single" (/resource/{uuid}) requests.

To implement the endpoint behaviour, you must override:

All the above are optional, but you must implement at least one of them. These methods must return a tuple of (status_code, body), where body is a dict.

Error handling is done automatically when an exception is raised.

Endpoint( resource: str, prefix: str = '/', param_key: str = None, related_model=None)
68    def __init__(self, resource: str, prefix: str = "/", param_key: str = None, related_model=None):
69        """
70        :param resource: The name of the resource. E.g `"/cities/"`
71        :param prefix: The prefix of the resource (useful when your resource is nested). E.g. `"/countries/{countryUuid}/regions/{regionUuid}/"`. If not provided it will be `"/"`.
72        :param related_model: The model class that this endpoint is related to. E.g. `City`. If not provided, it will be inferred from the class name.
73        :param param_key: The name of the path parameter. E.g. `"cityUuid"`. If not provided, it will be inferred from the related model.
74        """
75        self.related_model_name = related_model.__name__ if related_model else self.__class__.__name__
76        self.param_key = param_key if param_key else f"{snake_case_to_camelCase(self.related_model_name)}Uuid"
77        self.param = f"{{{self.param_key}}}"
78        self.resource = resource
79        self.prefix = prefix
80        self.parent_resources = remove_empties(self.prefix.split("/"))
81        self.path_params = [r.replace("{", "").replace("}", "") for r in self.parent_resources if r.startswith("{")]
82        self.global_endpoint = re.sub('/$', '', re.sub('/+', '/', self.prefix + self.resource))
83        self.single_endpoint = f"{self.global_endpoint}/{self.param}"
84        self.related_model = related_model
Parameters
  • resource: The name of the resource. E.g "/cities/"
  • prefix: The prefix of the resource (useful when your resource is nested). E.g. "/countries/{countryUuid}/regions/{regionUuid}/". If not provided it will be "/".
  • related_model: The model class that this endpoint is related to. E.g. City. If not provided, it will be inferred from the class name.
  • param_key: The name of the path parameter. E.g. "cityUuid". If not provided, it will be inferred from the related model.
DEFAULT_ENDPOINTS_DATA = {'default': {'integration_type': 'aws_proxy', 'response_content_type': 'application/json', 'request_content_type': 'application/json', 'response_mapping_template': '', 'request_mapping_template': '', 'response_mapping_regex': 'default'}}
related_model_name
param_key
param
resource
prefix
parent_resources
path_params
global_endpoint
single_endpoint
related_model
@classmethod
def global_is_array(cls) -> bool:
129    @classmethod
130    def global_is_array(cls) -> bool:  # pragma: unit
131        """
132        For OpenAPI spec generation.
133
134        Defaults to returning `True`.
135
136        Override this method to return `False` if the global endpoint should not return an array of items.
137
138        :return: `True` if the global endpoint should return an array of items.
139        """
140        return True

For OpenAPI spec generation.

Defaults to returning True.

Override this method to return False if the global endpoint should not return an array of items.

Returns

True if the global endpoint should return an array of items.

@classmethod
def has_custom_body_schema(cls) -> bool:
142    @classmethod
143    def has_custom_body_schema(cls) -> bool:  # pragma: no cover
144        """
145        For OpenAPI spec generation.
146
147        Defaults to returning `False`.
148
149        Override this method to return `True` if the expected schema of a request body is not derivable from the
150        endpoint's related model (see `__init__`).
151
152        :return: False when the expected schema of a request body is derivable from the endpoint's related model.
153        """
154        return False

For OpenAPI spec generation.

Defaults to returning False.

Override this method to return True if the expected schema of a request body is not derivable from the endpoint's related model (see __init__).

Returns

False when the expected schema of a request body is derivable from the endpoint's related model.

@classmethod
def has_custom_return_schema(cls) -> bool:
156    @classmethod
157    def has_custom_return_schema(cls) -> bool:  # pragma: no cover
158        """
159        For OpenAPI spec generation.
160
161        Defaults to returning `False`.
162
163        Override this method to return `True` if the returned object schema is not derivable from the endpoint's
164        related model (see `__init__`).
165
166        :return: False when the returned object schema is derivable from the endpoint's related model.
167        """
168        return False

For OpenAPI spec generation.

Defaults to returning False.

Override this method to return True if the returned object schema is not derivable from the endpoint's related model (see __init__).

Returns

False when the returned object schema is derivable from the endpoint's related model.

@classmethod
def get_body_schema(cls) -> str | dict:
170    @classmethod
171    def get_body_schema(cls) -> str | dict:  # pragma: no cover
172        """
173        For OpenAPI spec generation.
174
175        Defaults to returning `""` and instead derives the schema from the endpoint's related model (see `__init__`).
176
177        Override this method to return a yaml schema of what this endpoint expects in the request body.
178
179        :return: A string of a yaml schema.
180        """
181        return ""

For OpenAPI spec generation.

Defaults to returning "" and instead derives the schema from the endpoint's related model (see __init__).

Override this method to return a yaml schema of what this endpoint expects in the request body.

Returns

A string of a yaml schema.

@classmethod
def get_return_schema(cls) -> str | dict:
183    @classmethod
184    def get_return_schema(cls) -> str | dict:  # pragma: no cover
185        """
186        For OpenAPI spec generation.
187
188        Defaults to returning `""` and instead derives the schema from the endpoint's related model (see `__init__`).
189
190        Override this method to return a yaml schema of the object returned by this endpoint.
191
192        :return: A string of a yaml schema.
193        """
194        return ""

For OpenAPI spec generation.

Defaults to returning "" and instead derives the schema from the endpoint's related model (see __init__).

Override this method to return a yaml schema of the object returned by this endpoint.

Returns

A string of a yaml schema.

def handle_wakeup(self, event, context=None):
234    def handle_wakeup(self, event, context=None):  # pragma: unit
235        return 200, {"status": "OK"}
def handle_msk_event(self, event) -> tuple[int, dict]:
251    def handle_msk_event(self, event) -> tuple[int, dict]:  # pragma: no cover
252        """
253        Override this method to handle the MSK events going to lambda.
254
255        :param event: An event from AWS MSK.
256        :return: A tuple with the status code and the body.
257        """
258        raise NotImplementedException()

Override this method to handle the MSK events going to lambda.

Parameters
  • event: An event from AWS MSK.
Returns

A tuple with the status code and the body.

def get_global(self, event) -> tuple[int, dict]:
261    def get_global(self, event) -> tuple[int, dict]:  # pragma: unit
262        """
263        Override this method to handle `GET` requests to `/resource/`.
264
265        Usually this should return a list of resources.
266
267        :param event: An event from API Gateway.
268        :return: A tuple with the status code and the body.
269        """
270        raise NotImplementedException()

Override this method to handle GET requests to /resource/.

Usually this should return a list of resources.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and the body.

def get_single(self, event) -> tuple[int, dict]:
272    def get_single(self, event) -> tuple[int, dict]:  # pragma: unit
273        """
274        Override this method to handle `GET` requests to `/resource/{uuid}`.
275
276        Usually this should return one resource.
277
278        :param event: An event from API Gateway.
279        :return: A tuple with the status code and the body.
280        """
281        raise NotImplementedException()

Override this method to handle GET requests to /resource/{uuid}.

Usually this should return one resource.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and the body.

def get_global_generic( self, event, model_cls=None, belongs_to: dict = None, allow_listing_all: bool = False, order_by=None, order='asc', offset=None, limit=None, load_options=None) -> tuple[int, bedrock.endpoints.dto.bedrock_response.BedrockResponse]:
283    def get_global_generic(self,
284                           event,
285                           model_cls=None,
286                           belongs_to: dict = None,
287                           allow_listing_all: bool = False,
288                           order_by=None, order='asc',
289                           offset=None, limit=None,
290                           load_options=None) -> tuple[int, BedrockResponse]:  # pragma: integration
291        """
292        Use this method to automatically handle getting a list of `model_cls` resources.
293
294        :param event: An event from API Gateway.
295        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
296        :param belongs_to: A dictionary of key-value pairs that the items must match.
297        :param allow_listing_all: Whether to allow listing all items. Defaults to `False`.
298        :param order_by: The column to sort results of the query on, if any.
299        :param order: The order to sort results of the query in.
300        :param offset: The offset to use for pagination of results, if any.
301        :param limit: The number of rows to return in the results, if any.
302        :param load_options: The options to use for loading the results, if any.
303        :return: A tuple with the status code and a list of resources.
304        """
305        _model_cls = model_cls if model_cls else self.related_model
306        log.debug(f"Getting all {_model_cls.__name__} items (load_options: {load_options})")
307        try:
308            items, count = _model_cls.find_all_matching_criteria(belongs_to,
309                                                                 allow_listing_all=allow_listing_all,
310                                                                 order_by=order_by, order=order,
311                                                                 offset=offset, limit=limit,
312                                                                 load_options=load_options,
313                                                                 with_count=True)
314        except Exception as e:
315            raise NotFoundException(f'Unable to find {_model_cls.__name__} list') from e
316
317        response = [item.as_json() for item in items]
318        return 200, BedrockEnvelopedResponse(
319            response,
320            event,
321            PaginationMetadata(order_by, order, limit, offset, count)
322        )

Use this method to automatically handle getting a list of model_cls resources.

Parameters
  • event: An event from API Gateway.
  • model_cls: The model class that you want to manipulate. Defaults to self.related_model.
  • belongs_to: A dictionary of key-value pairs that the items must match.
  • allow_listing_all: Whether to allow listing all items. Defaults to False.
  • order_by: The column to sort results of the query on, if any.
  • order: The order to sort results of the query in.
  • offset: The offset to use for pagination of results, if any.
  • limit: The number of rows to return in the results, if any.
  • load_options: The options to use for loading the results, if any.
Returns

A tuple with the status code and a list of resources.

def get_single_generic( self, event, model_cls=None, belongs_to: dict = None, load_options=None, resource_id: str | dict = None) -> tuple[int, bedrock.endpoints.dto.bedrock_response.BedrockResponse]:
324    def get_single_generic(self,
325                           event,
326                           model_cls=None,
327                           belongs_to: dict = None,
328                           load_options=None,
329                           resource_id: str | dict = None) -> tuple[int, BedrockResponse]:  # pragma: integration
330        """
331        Use this method to automatically handle getting one `model_cls` resource.
332
333        :param event: An event from API Gateway.
334        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
335        :param belongs_to: A dictionary of key-value pairs that the items must match.
336        :param load_options: The options to use for loading the results, if any.
337        :param resource_id: Resource id that can be optionally passed in
338        :return: A tuple with the status code `200` and one resource.
339        """
340        _model_cls = model_cls if model_cls else self.related_model
341        if not resource_id:
342            try:
343                resource_id = event["pathParameters"][self.param_key]
344            except Exception as e:
345                raise NotFoundException(f"{self.param} not provided in url") from e
346
347        log.debug(f"Getting {_model_cls.__name__}: {resource_id}")
348        try:
349            response = _model_cls.get(resource_id,
350                                      restrictions=belongs_to,
351                                      load_options=load_options).as_json()
352        except Exception as e:
353            raise NotFoundException(
354                f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}') from e
355
356        return 200, BedrockEnvelopedResponse(response, event)

Use this method to automatically handle getting one model_cls resource.

Parameters
  • event: An event from API Gateway.
  • model_cls: The model class that you want to manipulate. Defaults to self.related_model.
  • belongs_to: A dictionary of key-value pairs that the items must match.
  • load_options: The options to use for loading the results, if any.
  • resource_id: Resource id that can be optionally passed in
Returns

A tuple with the status code 200 and one resource.

def post_global(self, event) -> tuple[int, dict]:
365    def post_global(self, event) -> tuple[int, dict]:  # pragma: unit
366        """
367        Override this method to handle `POST` requests to `/resource/`.
368
369        Usually this should create a new resource.
370
371        :param event: An event from API Gateway.
372        :return: A tuple with the status code and, typically, the body of what was just created.
373        """
374        raise NotImplementedException()

Override this method to handle POST requests to /resource/.

Usually this should create a new resource.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and, typically, the body of what was just created.

def post_single(self, event) -> tuple[int, dict]:
376    def post_single(self, event) -> tuple[int, dict]:  # pragma: unit
377        """
378        Override this method to handle `POST` requests to `/resource/{uuid}`.
379
380        This doesn't usually get any use, but feel free to override it if you need to.
381        It's here for completeness, but is fully functional.
382
383        :param event: An event from API Gateway.
384        :return: A tuple with the status code and the body.
385        """
386        raise NotImplementedException()

Override this method to handle POST requests to /resource/{uuid}.

This doesn't usually get any use, but feel free to override it if you need to. It's here for completeness, but is fully functional.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and the body.

def post_global_generic( self, event, model_cls=None, altered_body: dict = None, excludes=[], extra_objects={}, load_options=None, save_nested=False) -> tuple[int, bedrock.endpoints.dto.bedrock_response.BedrockEnvelopedResponse]:
388    def post_global_generic(self,
389                            event,
390                            model_cls=None,
391                            altered_body: dict = None,
392                            excludes=[],
393                            extra_objects={},
394                            load_options=None,
395                            save_nested=False) -> tuple[int, BedrockEnvelopedResponse]:  # pragma: integration
396        """
397        Use this method to automatically handle creating a new `model_cls` resource.
398        :param event: An event from API Gateway.
399        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
400        :param altered_body: Optionally, override the body provided by the request.
401        :param excludes: A list of keys to exclude from `model_cls` when returning a response.
402        :param extra_objects: A dictionary of key-value pairs to return with the response.
403        :param load_options: The options to use for loading the results on re-fetch, if any.
404        :param save_nested: Whether to update/create nested models provided in the body. Defaults to `False`.
405        :return: A tuple with status code `201` and the body of the created resource.
406        """
407        _model_cls = model_cls if model_cls else self.related_model
408        body = filter_body(event['body'], altered_body)
409        result = self._save_object_helper(_model_cls, body, excludes, extra_objects,
410                                          load_options=load_options, save_nested=save_nested)
411        return 201, BedrockEnvelopedResponse(result, event)

Use this method to automatically handle creating a new model_cls resource.

Parameters
  • event: An event from API Gateway.
  • model_cls: The model class that you want to manipulate. Defaults to self.related_model.
  • altered_body: Optionally, override the body provided by the request.
  • excludes: A list of keys to exclude from model_cls when returning a response.
  • extra_objects: A dictionary of key-value pairs to return with the response.
  • load_options: The options to use for loading the results on re-fetch, if any.
  • save_nested: Whether to update/create nested models provided in the body. Defaults to False.
Returns

A tuple with status code 201 and the body of the created resource.

def put_global(self, event) -> tuple[int, dict]:
420    def put_global(self, event) -> tuple[int, dict]:  # pragma: unit
421        """
422        Override this method to handle `PUT` requests to `/resource/`.
423
424        This doesn't usually get any use, but feel free to override it if you need to.
425        It's here for completeness, but is fully functional.
426
427        :param event: An event from API Gateway.
428        :return: A tuple with the status code and the body.
429        """
430        raise NotImplementedException()

Override this method to handle PUT requests to /resource/.

This doesn't usually get any use, but feel free to override it if you need to. It's here for completeness, but is fully functional.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and the body.

def put_single(self, event) -> tuple[int, dict]:
432    def put_single(self, event) -> tuple[int, dict]:  # pragma: unit
433        """
434        Override this method to handle `PUT` requests to `/resource/{uuid}`.
435
436        Usually this should edit an existing resource.
437
438        :param event: An event from API Gateway.
439        :return: A tuple with the status code and, typically, the body of what was just modified.
440        """
441        raise NotImplementedException()

Override this method to handle PUT requests to /resource/{uuid}.

Usually this should edit an existing resource.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and, typically, the body of what was just modified.

def put_single_generic( self, event, model_cls=None, unique_column: str | dict = None, altered_body: dict = None, belongs_to={}, excludes=[], extra_objects={}, load_options=None, save_nested=False) -> tuple[int, bedrock.endpoints.dto.bedrock_response.BedrockEnvelopedResponse]:
443    def put_single_generic(self,
444                           event,
445                           model_cls=None,
446                           unique_column: str | dict = None,
447                           altered_body: dict = None,
448                           belongs_to={},
449                           excludes=[],
450                           extra_objects={},
451                           load_options=None,
452                           save_nested=False) -> tuple[int, BedrockEnvelopedResponse]:  # pragma: integration
453        """
454        Use this method to automatically handle editing an existing `model_cls` resource.
455        :param event: An event from API Gateway.
456        :param model_cls: The model class that you want to manipulate. Defaults to `self.related_model`.
457        :param unique_column: The unique column for the `model_cls` (can be a dictionary if resource has a composite primary key). Defaults to `model_cls.get_unique_identifier_names()`.
458        :param altered_body: Optionally, override the body provided by the request.
459        :param belongs_to: A dictionary that restricts the model being edited by any parent resources it should belong to.
460        :param excludes: A list of keys to exclude from `model_cls` when returning a response.
461        :param extra_objects: A dictionary of key-value pairs to return with the response.
462        :param load_options: The options to use for loading the results, if any.
463        :param save_nested: Whether to update/create nested models provided in the body. Defaults to `False`.
464        :return: A tuple with status code `200` and the body of the edited resource.
465        """
466        _model_cls = model_cls if model_cls else self.related_model
467        body = filter_body(event['body'], altered_body)
468        objs = _model_cls.find_all_by_unique_column(body,
469                                                    unique_column=unique_column,
470                                                    restrictions=belongs_to,
471                                                    load_options=load_options)
472        if not objs or len(objs) == 0:
473            if _model_cls.should_create_if_not_found():
474                return self.post_global_generic(event, _model_cls, extra_objects=extra_objects, altered_body=body,
475                                                load_options=load_options, save_nested=save_nested)
476            else:
477                raise NotFoundException(f"{_model_cls.__name__} was not found")
478
479        obj = objs[0]
480        original_data = obj.as_json()
481
482        nested_model_keys = [m.key for m in _model_cls.get_nested_models()]
483        for key, value in body.items():
484            snake_key = camelCase_to_snake_case(key)
485            if snake_key not in nested_model_keys:
486                setattr(obj, snake_key, value)
487
488        result = self._save_object_helper(_model_cls, body, excludes, extra_objects, obj,
489                                          load_options=load_options, save_nested=save_nested)
490        return 200, BedrockEnvelopedResponse(result, event, original_data=original_data)

Use this method to automatically handle editing an existing model_cls resource.

Parameters
  • event: An event from API Gateway.
  • model_cls: The model class that you want to manipulate. Defaults to self.related_model.
  • unique_column: The unique column for the model_cls (can be a dictionary if resource has a composite primary key). Defaults to model_cls.get_unique_identifier_names().
  • altered_body: Optionally, override the body provided by the request.
  • belongs_to: A dictionary that restricts the model being edited by any parent resources it should belong to.
  • excludes: A list of keys to exclude from model_cls when returning a response.
  • extra_objects: A dictionary of key-value pairs to return with the response.
  • load_options: The options to use for loading the results, if any.
  • save_nested: Whether to update/create nested models provided in the body. Defaults to False.
Returns

A tuple with status code 200 and the body of the edited resource.

def delete_global(self, event) -> tuple[int, dict]:
499    def delete_global(self, event) -> tuple[int, dict]:  # pragma: unit
500        """
501        Override this method to handle `DELETE` requests to `/resource/`.
502
503        This doesn't usually get any use, but feel free to override it if you need to.
504        It's here for completeness, but is fully functional.
505
506        :param event: An event from API Gateway.
507        :return: A tuple with the status code and the body.
508        """
509        raise NotImplementedException()

Override this method to handle DELETE requests to /resource/.

This doesn't usually get any use, but feel free to override it if you need to. It's here for completeness, but is fully functional.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and the body.

def delete_single(self, event) -> tuple[int, dict]:
511    def delete_single(self, event) -> tuple[int, dict]:  # pragma: unit
512        """
513        Override this method to handle `DELETE` requests to `/resource/{uuid}`.
514
515        Usually this should delete an existing resource.
516
517        :param event: An event from API Gateway.
518        :return: A tuple with the status code and, typically, the body of what was just deleted.
519        """
520        raise NotImplementedException()

Override this method to handle DELETE requests to /resource/{uuid}.

Usually this should delete an existing resource.

Parameters
  • event: An event from API Gateway.
Returns

A tuple with the status code and, typically, the body of what was just deleted.

def delete_single_generic( self, event, model_cls=None, resource_id: str | dict = None, belongs_to: dict = None, load_options=None) -> tuple[int, bedrock.endpoints.dto.bedrock_response.BedrockDeletionResponse]:
522    def delete_single_generic(self,
523                              event,
524                              model_cls=None,
525                              resource_id: str | dict = None,
526                              belongs_to: dict = None,
527                              load_options=None) -> tuple[int, BedrockDeletionResponse]:  # pragma: integration
528        """
529        Use this method to automatically handle deleting an existing `model_cls` resource.
530
531        :param event: An event from API Gateway.
532        :param model_cls: The model class that you want to manipulate.
533        :param resource_id: The unique identifier of the resource to delete (can be a dictionary if resource has a composite primary key). Will throw an exception if not provided.
534        :param belongs_to: A dictionary that restricts the model being deleted by any parent resources it should belong to.
535        :param load_options: The options to use for loading the results, if any.
536        :return: A tuple with status code `202` and a body confirming the deletion accompanied by the original object.
537        """
538        _model_cls = model_cls if model_cls else self.related_model
539        if not resource_id:
540            raise BedrockException("Missing resource_id argument - if you're seeing this, the developer screwed up!")
541
542        log.debug(f"Deleting {_model_cls.__name__}: {resource_id}")
543        try:
544            obj = _model_cls.get(resource_id, restrictions=belongs_to, load_options=load_options)
545        except Exception as e:
546            raise NotFoundException(
547                f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}') from e
548        if obj is None:
549            raise NotFoundException(f'Unable to find {_model_cls.__name__} {event["pathParameters"][self.param_key]}')
550        json_obj = obj.as_json()
551        obj.delete()
552        return 202, BedrockDeletionResponse(json_obj, event)

Use this method to automatically handle deleting an existing model_cls resource.

Parameters
  • event: An event from API Gateway.
  • model_cls: The model class that you want to manipulate.
  • resource_id: The unique identifier of the resource to delete (can be a dictionary if resource has a composite primary key). Will throw an exception if not provided.
  • belongs_to: A dictionary that restricts the model being deleted by any parent resources it should belong to.
  • load_options: The options to use for loading the results, if any.
Returns

A tuple with status code 202 and a body confirming the deletion accompanied by the original object.