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)
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:
get_globalforGETrequests to/resource/get_singleforGETrequests to/resource/{uuid}post_globalforPOSTrequests to/resource/post_singleforPOSTrequests to/resource/{uuid}put_globalforPUTrequests to/resource/put_singleforPUTrequests to/resource/{uuid}delete_globalforDELETErequests to/resource/delete_singleforDELETErequests to/resource/{uuid}
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.
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.
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
Trueif the global endpoint should return an array of items.
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.
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.
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.
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.
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.
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.
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.
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.
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
200and one resource.
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.
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.
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_clswhen 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
201and the body of the created resource.
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.
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.
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 tomodel_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_clswhen 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
200and the body of the edited resource.
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.
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.
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
202and a body confirming the deletion accompanied by the original object.