insightconnect-plugin-runtime 5.4.8__tar.gz → 5.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {insightconnect_plugin_runtime-5.4.8/insightconnect_plugin_runtime.egg-info → insightconnect_plugin_runtime-5.5.0}/PKG-INFO +4 -2
  2. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/README.md +2 -0
  3. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/api/endpoints.py +33 -9
  4. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/clients/aws_client.py +8 -3
  5. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/exceptions.py +64 -8
  6. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/helper.py +259 -1
  7. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/plugin.py +25 -5
  8. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/server.py +37 -11
  9. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/util.py +3 -1
  10. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0/insightconnect_plugin_runtime.egg-info}/PKG-INFO +4 -2
  11. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime.egg-info/requires.txt +1 -1
  12. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/setup.py +2 -2
  13. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_helpers.py +215 -0
  14. insightconnect_plugin_runtime-5.5.0/tests/unit/utils.py +78 -0
  15. insightconnect_plugin_runtime-5.4.8/tests/unit/utils.py +0 -20
  16. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/MANIFEST.in +0 -0
  17. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect-plugin-swagger.json +0 -0
  18. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/__init__.py +0 -0
  19. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/action.py +0 -0
  20. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/api/__init__.py +0 -0
  21. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/api/schemas.py +0 -0
  22. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/cli.py +0 -0
  23. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/clients/__init__.py +0 -0
  24. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/clients/oauth.py +0 -0
  25. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/connection.py +0 -0
  26. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/data/input_message_schema.json +0 -0
  27. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/data/output_message_schema.json +0 -0
  28. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/dispatcher.py +0 -0
  29. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/metrics.py +0 -0
  30. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/schema.py +0 -0
  31. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/step.py +0 -0
  32. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/task.py +0 -0
  33. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/trigger.py +0 -0
  34. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime/variables.py +0 -0
  35. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime.egg-info/SOURCES.txt +0 -0
  36. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime.egg-info/dependency_links.txt +0 -0
  37. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/insightconnect_plugin_runtime.egg-info/top_level.txt +0 -0
  38. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/setup.cfg +0 -0
  39. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/__init__.py +0 -0
  40. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/__init__.py +0 -0
  41. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/__init__.py +0 -0
  42. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/__init__.py +0 -0
  43. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/__init__.py +0 -0
  44. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/__init__.py +0 -0
  45. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/hello/__init__.py +0 -0
  46. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/hello/action.py +0 -0
  47. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/hello/schema.py +0 -0
  48. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/return_bad_json/__init__.py +0 -0
  49. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/return_bad_json/action.py +0 -0
  50. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/return_bad_json/schema.py +0 -0
  51. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/throw_exception/__init__.py +0 -0
  52. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/throw_exception/action.py +0 -0
  53. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/actions/throw_exception/schema.py +0 -0
  54. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/connection/__init__.py +0 -0
  55. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/connection/connection.py +0 -0
  56. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/connection/schema.py +0 -0
  57. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/__init__.py +0 -0
  58. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/monitor_events/__init__.py +0 -0
  59. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/monitor_events/schema.py +0 -0
  60. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/tasks/monitor_events/task.py +0 -0
  61. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/__init__.py +0 -0
  62. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/__init__.py +0 -0
  63. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/schema.py +0 -0
  64. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/hello_trigger/trigger.py +0 -0
  65. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/__init__.py +0 -0
  66. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/schema.py +0 -0
  67. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/return_bad_json_trigger/trigger.py +0 -0
  68. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/__init__.py +0 -0
  69. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/schema.py +0 -0
  70. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/triggers/throw_exception_trigger/trigger.py +0 -0
  71. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/komand_hello_world/util/__init__.py +0 -0
  72. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/hello_world/setup.py +0 -0
  73. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/tests/__init__.py +0 -0
  74. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/tests/conftest.py +0 -0
  75. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/tests/test_cli.py +0 -0
  76. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/tests/test_hello_world.py +0 -0
  77. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/plugin/hello_world/tests/test_server.py +0 -0
  78. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/__init__.py +0 -0
  79. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_action.py +0 -0
  80. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_api.py +0 -0
  81. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_aws_action.py +0 -0
  82. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_custom_encoder.py +0 -0
  83. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_endpoints.py +0 -0
  84. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_exceptions.py +0 -0
  85. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_metrics.py +0 -0
  86. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_oauth.py +0 -0
  87. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_plugin.py +0 -0
  88. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_schema.py +0 -0
  89. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_server_cloud_plugins.py +0 -0
  90. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_server_spec.py +0 -0
  91. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_trigger.py +0 -0
  92. {insightconnect_plugin_runtime-5.4.8 → insightconnect_plugin_runtime-5.5.0}/tests/unit/test_variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: insightconnect-plugin-runtime
3
- Version: 5.4.8
3
+ Version: 5.5.0
4
4
  Summary: InsightConnect Plugin Runtime
5
5
  Home-page: https://github.com/rapid7/komand-plugin-sdk-python
6
6
  Author: Rapid7 Integrations Alliance
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Topic :: Software Development :: Build Tools
14
14
  Description-Content-Type: text/markdown
15
- Requires-Dist: requests==2.31.0
15
+ Requires-Dist: requests==2.32.0
16
16
  Requires-Dist: python_jsonschema_objects==0.5.2
17
17
  Requires-Dist: jsonschema==4.21.1
18
18
  Requires-Dist: certifi==2024.2.2
@@ -211,6 +211,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
211
211
  after cloning this repository.
212
212
 
213
213
  ## Changelog
214
+ * 5.5.0 - Updated helper class to add `make_request`, `response_handler`, `extract_json`, and `request_error_handling` for HTTP requests, and `hash_sha1` and `compare_and_dedupe_hashes` to provide support for hash comparisons | Add `METHOD_NOT_ALLOWED`, `CONFLICT`, `REDIRECT_ERROR`, and `CONNECTION_ERROR` to PluginException presets
215
+ * 5.4.9 - Updated aws_client to clean assume role json object to remove any none or empty string values.
214
216
  * 5.4.8 - Address vulnerabilities within `gunicorn` and `idna` python packages.
215
217
  * 5.4.7 - Address vulnerabilities within `insightconnect-python-3-plugin` image.
216
218
  * 5.4.6 - Updated our cloud loggers to include the request path | Updated slim image to remediate vulnerabilities.
@@ -182,6 +182,8 @@ contributed. Black is installed as a test dependency and the hook can be initial
182
182
  after cloning this repository.
183
183
 
184
184
  ## Changelog
185
+ * 5.5.0 - Updated helper class to add `make_request`, `response_handler`, `extract_json`, and `request_error_handling` for HTTP requests, and `hash_sha1` and `compare_and_dedupe_hashes` to provide support for hash comparisons | Add `METHOD_NOT_ALLOWED`, `CONFLICT`, `REDIRECT_ERROR`, and `CONNECTION_ERROR` to PluginException presets
186
+ * 5.4.9 - Updated aws_client to clean assume role json object to remove any none or empty string values.
185
187
  * 5.4.8 - Address vulnerabilities within `gunicorn` and `idna` python packages.
186
188
  * 5.4.7 - Address vulnerabilities within `insightconnect-python-3-plugin` image.
187
189
  * 5.4.6 - Updated our cloud loggers to include the request path | Updated slim image to remediate vulnerabilities.
@@ -83,7 +83,17 @@ def handle_errors(error: HTTPException):
83
83
 
84
84
 
85
85
  class Endpoints:
86
- def __init__(self, logger, plugin, spec, debug, workers, threads, master_pid, config_options=None):
86
+ def __init__(
87
+ self,
88
+ logger,
89
+ plugin,
90
+ spec,
91
+ debug,
92
+ workers,
93
+ threads,
94
+ master_pid,
95
+ config_options=None,
96
+ ):
87
97
  self.plugin = plugin
88
98
  self.logger = structlog.get_logger("plugin")
89
99
  self.spec = spec
@@ -167,7 +177,9 @@ class Endpoints:
167
177
  Endpoints.validate_action_trigger_task_name(input_message, name, "task")
168
178
  # No validation on the plugin custom config to leave this as configurable as possible.
169
179
  # `add_plugin_custom_config` will pass any available values to the plugin for interpretation.
170
- input_message = self.add_plugin_custom_config(input_message, request.headers.get(ORG_ID))
180
+ input_message = self.add_plugin_custom_config(
181
+ input_message, request.headers.get(ORG_ID)
182
+ )
171
183
  output = self.run_action_trigger_task(input_message)
172
184
  self.logger.info("Plugin task finished execution...")
173
185
  return output
@@ -750,7 +762,9 @@ class Endpoints:
750
762
 
751
763
  return version
752
764
 
753
- def add_plugin_custom_config(self, input_data: Dict[str, Any], org_id: str) -> Dict[str, Any]:
765
+ def add_plugin_custom_config(
766
+ self, input_data: Dict[str, Any], org_id: str
767
+ ) -> Dict[str, Any]:
754
768
  """
755
769
  Using the retrieved configs pulled from komand-props, pass the configuration that matches the requesting
756
770
  Org ID which is passed to the task via a header (`X-IPIMS-ORGID`) from the plugin sidecar.
@@ -763,17 +777,27 @@ class Endpoints:
763
777
  - all orgs for default runs will poll back 12 hours.
764
778
  - all orgs in lookback mode will be 100 hours (task triggered with no state).
765
779
  """
766
- additional_config = self.config_options.get(org_id) or self.config_options.get("*")
780
+ additional_config = self.config_options.get(org_id) or self.config_options.get(
781
+ "*"
782
+ )
767
783
  if additional_config:
768
- self.logger.info("Found config options; adding this to the request parameters...")
769
- additional_config = additional_config.copy() # copy to preserve the referenced value in self.config_options
784
+ self.logger.info(
785
+ "Found config options; adding this to the request parameters..."
786
+ )
787
+ additional_config = (
788
+ additional_config.copy()
789
+ ) # copy to preserve the referenced value in self.config_options
770
790
  # As a safeguard we only pass the lookback config params if the plugin has no state
771
791
  # This means we still need to manually delete the state for plugins on a per org basis.
772
792
  # This also means first time customers for their 'initial' lookup would get the lookback value passed in.
773
- if input_data.get("body", {}).get("state") and additional_config.get("lookback"):
774
- self.logger.info("Found an existing plugin state, not passing lookback value...")
793
+ if input_data.get("body", {}).get("state") and additional_config.get(
794
+ "lookback"
795
+ ):
796
+ self.logger.info(
797
+ "Found an existing plugin state, not passing lookback value..."
798
+ )
775
799
  del additional_config["lookback"]
776
- input_data.get("body", {}).update({'custom_config': additional_config})
800
+ input_data.get("body", {}).update({"custom_config": additional_config})
777
801
  self.logger.info(f"Custom config being sent to plugin: {additional_config}")
778
802
 
779
803
  return input_data
@@ -15,6 +15,7 @@ from botocore.exceptions import ClientError
15
15
  import insightconnect_plugin_runtime
16
16
  from insightconnect_plugin_runtime.action import Action
17
17
  from insightconnect_plugin_runtime.exceptions import PluginException
18
+ from insightconnect_plugin_runtime.helper import clean
18
19
 
19
20
  REGION = "region"
20
21
  EXTERNAL_ID = "external_id"
@@ -541,9 +542,13 @@ class AWSAction(Action):
541
542
  sts_client = boto3.client("sts", **auth_params)
542
543
  try:
543
544
  assumed_role_object = sts_client.assume_role(
544
- RoleArn=assume_role_params.get(ROLE_ARN),
545
- RoleSessionName=session_name,
546
- ExternalId=assume_role_params.get(EXTERNAL_ID),
545
+ **clean(
546
+ {
547
+ "RoleArn": assume_role_params.get(ROLE_ARN),
548
+ "RoleSessionName": session_name,
549
+ "ExternalId": assume_role_params.get(EXTERNAL_ID),
550
+ }
551
+ )
547
552
  )
548
553
  except ClientError as error:
549
554
  raise PluginException(
@@ -1,6 +1,56 @@
1
1
  # -*- coding: utf-8 -*-
2
- import structlog
3
- logger = structlog.get_logger("plugin")
2
+ class ResponseExceptionData:
3
+ RESPONSE_TEXT = "response_text"
4
+ RESPONSE_JSON = "response_json"
5
+ RESPONSE = "response"
6
+ EXCEPTION = "exception"
7
+
8
+
9
+ class HTTPStatusCodes:
10
+
11
+ # 4xx Client Errors
12
+ BAD_REQUEST = 400
13
+ UNAUTHORIZED = 401
14
+ PAYMENT_REQUIRED = 402
15
+ FORBIDDEN = 403
16
+ NOT_FOUND = 404
17
+ METHOD_NOT_ALLOWED = 405
18
+ NOT_ACCEPTABLE = 406
19
+ PROXY_AUTHENTICATION_REQUIRED = 407
20
+ REQUEST_TIMEOUT = 408
21
+ CONFLICT = 409
22
+ GONE = 410
23
+ LENGTH_REQUIRED = 411
24
+ PRECONDITION_FAILED = 412
25
+ PAYLOAD_TOO_LARGE = 413
26
+ URI_TOO_LONG = 414
27
+ UNSUPPORTED_MEDIA_TYPE = 415
28
+ RANGE_NOT_SATISFIABLE = 416
29
+ EXPECTATION_FAILED = 417
30
+ I_AM_A_TEAPOT = 418
31
+ MISDIRECTED_REQUEST = 421
32
+ UNPROCESSABLE_ENTITY = 422
33
+ LOCKED = 423
34
+ FAILED_DEPENDENCY = 424
35
+ TOO_EARLY = 425
36
+ UPGRADE_REQUIRED = 426
37
+ PRECONDITION_REQUIRED = 428
38
+ TOO_MANY_REQUESTS = 429
39
+ REQUEST_HEADER_FIELDS_TOO_LARGE = 431
40
+ UNAVAILABLE_FOR_LEGAL_REASONS = 451
41
+
42
+ # 5xx Server Errors
43
+ INTERNAL_SERVER_ERROR = 500
44
+ NOT_IMPLEMENTED = 501
45
+ BAD_GATEWAY = 502
46
+ SERVICE_UNAVAILABLE = 503
47
+ GATEWAY_TIMEOUT = 504
48
+ HTTP_VERSION_NOT_SUPPORTED = 505
49
+ VARIANT_ALSO_NEGOTIATES = 506
50
+ INSUFFICIENT_STORAGE = 507
51
+ LOOP_DETECTED = 508
52
+ NOT_EXTENDED = 510
53
+ NETWORK_AUTHENTICATION_REQUIRED = 511
4
54
 
5
55
 
6
56
  class ClientException(Exception):
@@ -65,6 +115,10 @@ class ConnectionTestException(Exception):
65
115
  TIMEOUT = "timeout"
66
116
  BAD_REQUEST = "bad_request"
67
117
  INVALID_CREDENTIALS = "invalid_credentials"
118
+ METHOD_NOT_ALLOWED = "method_not_allowed"
119
+ CONFLICT = "conflict"
120
+ CONNECTION_ERROR = "connection_error"
121
+ REDIRECT_ERROR = "redirect_error"
68
122
 
69
123
  # Dictionary of cause messages
70
124
  causes = {
@@ -82,6 +136,10 @@ class ConnectionTestException(Exception):
82
136
  Preset.TIMEOUT: "The connection timed out.",
83
137
  Preset.BAD_REQUEST: "The server is unable to process the request.",
84
138
  Preset.INVALID_CREDENTIALS: "Authentication failed: invalid credentials.",
139
+ Preset.METHOD_NOT_ALLOWED: "The request method is not allowed for this resource.",
140
+ Preset.CONFLICT: "Request cannot be completed due to a conflict with the current state of the resource.",
141
+ Preset.CONNECTION_ERROR: "Failed to connect to the server.",
142
+ Preset.REDIRECT_ERROR: "Request redirected more than the set limit for the server.",
85
143
  }
86
144
 
87
145
  # Dictionary of assistance/remediation messages
@@ -103,6 +161,10 @@ class ConnectionTestException(Exception):
103
161
  Preset.BAD_REQUEST: "Verify your plugin input is correct and not malformed and try again. "
104
162
  "If the issue persists, please contact support.",
105
163
  Preset.INVALID_CREDENTIALS: "Please verify the credentials for your account and try again.",
164
+ Preset.METHOD_NOT_ALLOWED: "Please try a supported method for this resource.",
165
+ Preset.CONFLICT: "Please check your request, and try again.",
166
+ Preset.CONNECTION_ERROR: "Please check your network connection and try again.",
167
+ Preset.REDIRECT_ERROR: "Please check your request and try again.",
106
168
  }
107
169
 
108
170
  def __init__(self, cause=None, assistance=None, data=None, preset=None):
@@ -124,12 +186,6 @@ class ConnectionTestException(Exception):
124
186
 
125
187
  self.data = str(data) if data else ""
126
188
 
127
- # Safeguard to ensure the exception is logged across all plugins even if the plugin
128
- # itself does not call `self.logger.error(<error info>)`
129
- params = ["cause", "assistance", "data", "preset"]
130
- info_log = ", ".join([f"{atr}='{getattr(self, atr)}'" for atr in params if getattr(self, atr)])
131
- logger.error(f"Plugin exception instantiated. {info_log}")
132
-
133
189
  def __str__(self):
134
190
  if self.data:
135
191
  return "Connection test failed!\n\n{cause} {assistance} Response was: {data}".format(
@@ -12,18 +12,276 @@ from datetime import datetime, timedelta
12
12
  from io import IOBase
13
13
  from typing import Any, Callable, Dict, List, Union
14
14
  from urllib import request
15
+ from hashlib import sha1
16
+ from json import JSONDecodeError
15
17
 
16
18
  import requests
17
19
 
18
- from insightconnect_plugin_runtime.exceptions import PluginException
20
+ from insightconnect_plugin_runtime.exceptions import (
21
+ PluginException,
22
+ HTTPStatusCodes,
23
+ ResponseExceptionData,
24
+ )
19
25
 
20
26
  CAMEL_CASE_REGEX = r"\b[a-z0-9]+([A-Z][a-z]+[0-9]*)*\b"
21
27
  CAMEL_CASE_ACRONYM_REGEX = r"\b[a-z0-9]+([A-Z]+[0-9]*)*\b"
22
28
  PASCAL_CASE_REGEX = r"\b[A-Z][a-z]+[0-9]*([A-Z][a-z]+[0-9]*)*\b"
29
+ ENCODE_TYPE = "utf-8"
23
30
 
24
31
  DEFAULTS_HOURS_AGO = 24
25
32
 
26
33
 
34
+ def hash_sha1(log: dict) -> str:
35
+ """
36
+ Iterate through a dictionary and hash each value.
37
+ :param log: Dictionary to be hashed.
38
+ :type dict:
39
+ :return: Hex digest of hash.
40
+ :rtype: str
41
+ """
42
+ hash_ = sha1() # nosec B303
43
+ for key, value in log.items():
44
+ hash_.update(f"{key}{value}".encode(ENCODE_TYPE))
45
+ return hash_.hexdigest()
46
+
47
+
48
+ def compare_and_dedupe_hashes(
49
+ previous_logs_hashes: list, new_logs: list
50
+ ) -> tuple[list, list]:
51
+ """
52
+ Iterate through two lists of values, hashing each. Compare hash value to a list of existing hash values.
53
+ If the hash exists, return both it and the value in separate lists once iterated.
54
+ :param previous_logs_hashes: List of existing hashes to compare against.
55
+ :type list:
56
+ :param new_logs: New values to hash and compare to existing list of hashes.
57
+ :type list:
58
+ :return: Hex digest of hash.
59
+ :rtype: tuple[list, list]
60
+ """
61
+ new_logs_hashes = []
62
+ logs_to_return = []
63
+ for log in new_logs:
64
+ hash_ = hash_sha1(log)
65
+ if hash_ not in previous_logs_hashes:
66
+ new_logs_hashes.append(hash_)
67
+ logs_to_return.append(log)
68
+ logging.info(
69
+ f"Original number of logs:{len(new_logs)}. Number of logs after de-duplication:{len(logs_to_return)}"
70
+ )
71
+ return logs_to_return, new_logs_hashes
72
+
73
+
74
+ def make_request(
75
+ _request: requests.Request,
76
+ timeout: int = 60,
77
+ verify: bool = True,
78
+ cert: Union[str, tuple]=None,
79
+ stream: bool = False,
80
+ allow_redirects: bool = True,
81
+ exception_custom_configs: Dict[int, Exception]={},
82
+ exception_data_location: str = None,
83
+ allowed_status_codes: list[str] = [],
84
+ ) -> tuple[requests.Response, dict]:
85
+ """
86
+ Makes a HTTP request while checking for RequestErrors and JSONDecodeErrors
87
+ Returns the request response and the response JSON if required.
88
+ :param _request: Request object to utilize in request
89
+ :type Request:
90
+ :param timeout: Requests timeout paramater
91
+ :type int:
92
+ :param verify: Whether to verify the server's TLS certificate
93
+ :type bool:
94
+ :param cert: Certificate to include with request, str location or key/value pair
95
+ :type Union[str, dict]:
96
+ :param stream: Whether to immediately download the response content
97
+ :type bool:
98
+ :param allow_redirects: Set to true by default
99
+ :type bool:
100
+ :param exception_custom_configs: Custom exception values to be raised per HTTPStatusCode.
101
+ :type Dict[str, Exception]:
102
+ :param exception_data_location: Where the returned data should be retrieved. Can provide ResponseExceptionData values.
103
+ :type str:
104
+ :param allowed_status_codes: Status codes that will not raise an exception.
105
+ :type list[str]:
106
+
107
+ :return: The request response and the response JSON.
108
+ :rtype: tuple[Response, dict]
109
+ """
110
+ try:
111
+ with requests.Session() as session:
112
+ prepared_request = session.prepare_request(request=_request)
113
+ response = session.send(
114
+ prepared_request,
115
+ verify=verify,
116
+ timeout=timeout,
117
+ allow_redirects=allow_redirects,
118
+ cert=cert,
119
+ stream=stream,
120
+ )
121
+ except requests.exceptions.Timeout as exception:
122
+ raise PluginException(
123
+ preset=PluginException.Preset.TIMEOUT, data=str(exception)
124
+ )
125
+ except requests.exceptions.ConnectionError as exception:
126
+ raise PluginException(
127
+ preset=PluginException.Preset.CONNECTION_ERROR, data=str(exception)
128
+ )
129
+ except requests.exceptions.TooManyRedirects as exception:
130
+ raise PluginException(
131
+ preset=PluginException.Preset.REDIRECT_ERROR, data=str(exception)
132
+ )
133
+ except requests.exceptions.RequestException as exception:
134
+ if not isinstance(exception, requests.exceptions.HTTPError):
135
+ raise PluginException(
136
+ preset=PluginException.Preset.UNKNOWN, data=str(exception)
137
+ )
138
+ response_handler(response, exception_custom_configs, exception_data_location, allowed_status_codes)
139
+ return response
140
+
141
+
142
+ def extract_json(response: requests.Response) -> dict:
143
+ """Extract JSON from a request object while error handling a JSONDecodeError.
144
+ :param response: Response object ot utilize in extract
145
+ :type Response:
146
+ :returns: Dictionary of response JSON
147
+ :rtype: Dict
148
+ """
149
+ try:
150
+ response_json = response.json()
151
+ return response_json
152
+ except JSONDecodeError as exception:
153
+ raise PluginException(
154
+ preset=PluginException.Preset.INVALID_JSON, data=str(exception)
155
+ )
156
+
157
+
158
+ def request_error_handling() -> Union[Any, None]:
159
+ """request_error_handling. This decorator allows a method that makes a request to complete with error handling.
160
+ A plugin exception will be raised whenever an error is caught. Response.raise_for_status() must be called in the
161
+ wrapped method to handle HTTPErrors.
162
+
163
+ :returns: API call function data or None.
164
+ :rtype: Union[Any, None]
165
+ """
166
+
167
+ def _decorate(func: Callable):
168
+ def _wrapper(self, *args, **kwargs):
169
+ try:
170
+ return func(self, *args, **kwargs)
171
+ except requests.exceptions.Timeout as exception:
172
+ raise PluginException(
173
+ preset=PluginException.Preset.TIMEOUT, data=str(exception)
174
+ )
175
+ except requests.exceptions.ConnectionError as exception:
176
+ raise PluginException(
177
+ preset=PluginException.Preset.CONNECTION_ERROR, data=str(exception)
178
+ )
179
+ except requests.exceptions.TooManyRedirects as exception:
180
+ raise PluginException(
181
+ preset=PluginException.Preset.REDIRECT_ERROR, data=str(exception)
182
+ )
183
+ except requests.exceptions.RequestException as exception:
184
+ if isinstance(exception, requests.exceptions.HTTPError):
185
+ response_handler(exception.response)
186
+ else:
187
+ raise PluginException(
188
+ preset=PluginException.Preset.UNKNOWN, data=str(exception)
189
+ )
190
+
191
+ return _wrapper
192
+
193
+ return _decorate
194
+
195
+
196
+ def response_handler(
197
+ response: requests.Response,
198
+ custom_configs: Dict[int, Exception]={},
199
+ data_location: str = None,
200
+ allowed_status_codes: list[str] = [],
201
+ ) -> None:
202
+ """
203
+ Check response status codes and return a generic PluginException preset if a HTTPError is raised.
204
+ Excetion cause, assistance, and data can be overwritten by supplied parameters.
205
+
206
+ :param response: Response object whose status should be checked.
207
+ :type Response:
208
+ :param custom_configs: Custom exception values to be raised per HTTPStatusCode.
209
+ :type Dict[str, Exception]:
210
+ :param data_location: Where the returned data should be retrieved. Can provide ResponseExceptionData values.
211
+ :type str:
212
+ :param allowed_status_codes: Status codes that will not raise an exception.
213
+ :type list[str]:
214
+
215
+ :return: None.
216
+ :rtype: None
217
+ """
218
+ try:
219
+ response.raise_for_status()
220
+ except requests.exceptions.HTTPError as exception:
221
+
222
+ data = _return_response_data(response, exception, data_location)
223
+ status_code = response.status_code
224
+ if status_code in allowed_status_codes:
225
+ return
226
+ status_code_presets = {
227
+ HTTPStatusCodes.BAD_REQUEST: PluginException.Preset.BAD_REQUEST,
228
+ HTTPStatusCodes.UNAUTHORIZED: PluginException.Preset.INVALID_CREDENTIALS,
229
+ HTTPStatusCodes.FORBIDDEN: PluginException.Preset.UNAUTHORIZED,
230
+ HTTPStatusCodes.NOT_FOUND: PluginException.Preset.NOT_FOUND,
231
+ HTTPStatusCodes.METHOD_NOT_ALLOWED: PluginException.Preset.METHOD_NOT_ALLOWED,
232
+ HTTPStatusCodes.REQUEST_TIMEOUT: PluginException.Preset.TIMEOUT,
233
+ HTTPStatusCodes.CONFLICT: PluginException.Preset.CONFLICT,
234
+ HTTPStatusCodes.TOO_MANY_REQUESTS: PluginException.Preset.RATE_LIMIT,
235
+ HTTPStatusCodes.INTERNAL_SERVER_ERROR: PluginException.Preset.SERVER_ERROR,
236
+ HTTPStatusCodes.SERVICE_UNAVAILABLE: PluginException.Preset.SERVICE_UNAVAILABLE,
237
+ }
238
+ status_code_preset = status_code_presets.get(status_code)
239
+ exception = PluginException(preset=PluginException.Preset.UNKNOWN, data=data)
240
+ logging.info(f"Request to {response.url} failed. Status code: {status_code}")
241
+ if status_code in custom_configs.keys():
242
+ exception = custom_configs.get(status_code)
243
+ if hasattr(exception, "data") and data is not None:
244
+ exception.data = data
245
+ elif status_code_preset:
246
+ exception = PluginException(preset=status_code_preset, data=data)
247
+
248
+ raise exception
249
+
250
+
251
+ def _return_response_data(
252
+ response: requests.Response,
253
+ exception: requests.HTTPError,
254
+ data_location: str = None,
255
+ ) -> str:
256
+ """
257
+ Retrieve data from HTTP Error given a provided data location
258
+
259
+ :param response: Response object whose status should be checked.
260
+ :type Response:
261
+ :param exception: HTTPError exception to retrieve data.
262
+ :type HTTPError:
263
+ :param data_location: Where the returned data should be retrieved. Can provide ResponseExceptionData values.
264
+ :type str:
265
+
266
+ :return: Exception data.
267
+ :rtype: str
268
+ """
269
+ data = None
270
+ if data_location == ResponseExceptionData.EXCEPTION:
271
+ data = str(exception)
272
+ elif data_location == ResponseExceptionData.RESPONSE:
273
+ data = response
274
+ elif data_location == ResponseExceptionData.RESPONSE_TEXT:
275
+ data = response.text
276
+ elif data_location == ResponseExceptionData.RESPONSE_JSON:
277
+ try:
278
+ data = response.json()
279
+ except JSONDecodeError:
280
+ # Return full exception if JSON cannot be resolved
281
+ data = exception
282
+ return data
283
+
284
+
27
285
  def extract_value(begin, key, end, s):
28
286
  """
29
287
  Returns a string from a given key/pattern using provided regular expressions.
@@ -18,7 +18,12 @@ from insightconnect_plugin_runtime.exceptions import (
18
18
  PluginException,
19
19
  )
20
20
  from insightconnect_plugin_runtime.metrics import MetricsBuilder
21
- from insightconnect_plugin_runtime.util import is_running_in_cloud, flush_logging_handlers
21
+ from insightconnect_plugin_runtime.util import (
22
+ is_running_in_cloud,
23
+ flush_logging_handlers,
24
+ )
25
+
26
+ logger = structlog.get_logger("plugin")
22
27
 
23
28
  message_output_type = {
24
29
  "action_start": "action_event",
@@ -222,7 +227,9 @@ class Plugin(object):
222
227
  if status_code is not None:
223
228
  output_message["status_code"] = status_code
224
229
 
225
- if error_object is not None and isinstance(error_object, (ConnectionTestException, PluginException)):
230
+ if error_object is not None and isinstance(
231
+ error_object, (ConnectionTestException, PluginException)
232
+ ):
226
233
  output_message["exception"] = {
227
234
  "cause": error_object.cause,
228
235
  "assistance": error_object.assistance,
@@ -236,6 +243,9 @@ class Plugin(object):
236
243
 
237
244
  if ex:
238
245
  if isinstance(ex, ConnectionTestException):
246
+ error_data = ex.data if ex.data else None
247
+ info_log = f"cause={ex.cause}, assistance={ex.assistance}, data={error_data}"
248
+ logger.error(f"Plugin exception raised. {info_log}")
239
249
  output_message["exception"] = {
240
250
  "cause": ex.cause,
241
251
  "assistance": ex.assistance,
@@ -441,7 +451,13 @@ class Plugin(object):
441
451
  is_debug,
442
452
  )
443
453
  else:
444
- output, state, has_more_pages, status_code, error_object = self.start_step(
454
+ (
455
+ output,
456
+ state,
457
+ has_more_pages,
458
+ status_code,
459
+ error_object,
460
+ ) = self.start_step(
445
461
  input_message["body"],
446
462
  "task",
447
463
  struct_logger,
@@ -571,7 +587,9 @@ class Plugin(object):
571
587
  if step_key == "task":
572
588
  state = message_body["state"]
573
589
  step.state.validate(state)
574
- custom_config = message_body.get("custom_config", {}) # we don't validate this for now
590
+ custom_config = message_body.get(
591
+ "custom_config", {}
592
+ ) # we don't validate this for now
575
593
 
576
594
  # Validate required inputs
577
595
  # Step inputs will be checked against schema for required properties existence
@@ -611,7 +629,9 @@ class Plugin(object):
611
629
  parameters = inspect.signature(func)
612
630
  if len(parameters.parameters) > 0:
613
631
  if step_key == "task" and not is_test:
614
- output, state, has_more_pages, status_code, error_object = func(params, state, custom_config)
632
+ output, state, has_more_pages, status_code, error_object = func(
633
+ params, state, custom_config
634
+ )
615
635
  else:
616
636
  output = func(params)
617
637
  else:
@@ -14,7 +14,13 @@ from gunicorn.arbiter import Arbiter
14
14
  import structlog
15
15
  from pythonjsonlogger.jsonlogger import JsonFormatter
16
16
  from requests import get as request_get
17
- from requests.exceptions import HTTPError, MissingSchema, Timeout, JSONDecodeError, ConnectionError
17
+ from requests.exceptions import (
18
+ HTTPError,
19
+ MissingSchema,
20
+ Timeout,
21
+ JSONDecodeError,
22
+ ConnectionError,
23
+ )
18
24
  from time import sleep
19
25
  from werkzeug.utils import secure_filename
20
26
 
@@ -43,8 +49,12 @@ OPEN_API_VERSION = "2.0"
43
49
  VERSION_MAPPING = {"legacy": "/", "v1": "/api/v1"}
44
50
  CPS_RETRY = 5
45
51
  RETRY_SLEEP = 10
46
- CPS_ENDPOINT = os.getenv("CPS_ENDPOINT") # endpoint to retrieve plugin custom configs (set in cloud deployments.tf)
47
- DEFAULT_SCHEDULE_INTERVAL_MINUTES = 3 # default to 3 as most integrations run on 5 minute interval
52
+ CPS_ENDPOINT = os.getenv(
53
+ "CPS_ENDPOINT"
54
+ ) # endpoint to retrieve plugin custom configs (set in cloud deployments.tf)
55
+ DEFAULT_SCHEDULE_INTERVAL_MINUTES = (
56
+ 3 # default to 3 as most integrations run on 5 minute interval
57
+ )
48
58
 
49
59
 
50
60
  class PluginServer(gunicorn.app.base.BaseApplication):
@@ -185,32 +195,48 @@ class PluginServer(gunicorn.app.base.BaseApplication):
185
195
  # Call out to komand-props to get configurations related to only the plugin pod running.
186
196
  if is_running_in_cloud() and self.plugin.tasks:
187
197
  for attempt in range(1, CPS_RETRY + 1):
188
- self.logger.info(f"Getting plugin configuration information... (attempt {attempt}/{CPS_RETRY})")
198
+ self.logger.info(
199
+ f"Getting plugin configuration information... (attempt {attempt}/{CPS_RETRY})"
200
+ )
189
201
  try:
190
202
  request_response = request_get(CPS_ENDPOINT, timeout=30)
191
203
  resp_json = request_response.json()
192
- plugin = self.plugin.name.lower().replace(" ", "_") # match how we name our images
204
+ plugin = self.plugin.name.lower().replace(
205
+ " ", "_"
206
+ ) # match how we name our images
193
207
  plugin_config = resp_json.get("plugins", {}).get(plugin, {})
194
208
 
195
209
  self.config_options = plugin_config
196
210
  self.logger.info("Plugin configuration successfully retrieved...")
197
211
  return
198
212
  except MissingSchema as missing_schema:
199
- self.logger.error(f"Invalid URL being requested: {CPS_ENDPOINT}, error={missing_schema}")
213
+ self.logger.error(
214
+ f"Invalid URL being requested: {CPS_ENDPOINT}, error={missing_schema}"
215
+ )
200
216
  except Timeout as timeout:
201
- self.logger.error(f"Connection timeout hit. CPS={CPS_ENDPOINT}, error={timeout}")
217
+ self.logger.error(
218
+ f"Connection timeout hit. CPS={CPS_ENDPOINT}, error={timeout}"
219
+ )
202
220
  except HTTPError as http_error:
203
- self.logger.error(f"Connection error when trying to reach CPS. CPS={CPS_ENDPOINT}, error={http_error}")
221
+ self.logger.error(
222
+ f"Connection error when trying to reach CPS. CPS={CPS_ENDPOINT}, error={http_error}"
223
+ )
204
224
  except JSONDecodeError as bad_json:
205
- self.logger.error(f"Got bad JSON back. Response content={request_response.content}, error={bad_json}")
225
+ self.logger.error(
226
+ f"Got bad JSON back. Response content={request_response.content}, error={bad_json}"
227
+ )
206
228
  except ConnectionError as http_connection_pool_error:
207
229
  self.logger.info(
208
230
  "Connection refused when trying to reach CPS, retrying as the connection is still establishing."
209
231
  )
210
232
  except Exception as error:
211
- self.logger.error(f"Hit an unexpected error when retrieving plugin custom configs, error={error}")
233
+ self.logger.error(
234
+ f"Hit an unexpected error when retrieving plugin custom configs, error={error}"
235
+ )
212
236
  sleep(RETRY_SLEEP)
213
- self.logger.error(f"Unable to reach CPS after {CPS_RETRY} attempts. CPS={CPS_ENDPOINT}")
237
+ self.logger.error(
238
+ f"Unable to reach CPS after {CPS_RETRY} attempts. CPS={CPS_ENDPOINT}"
239
+ )
214
240
 
215
241
  def register_api_spec(self):
216
242
  """Register all swagger schema definitions and path objects"""