isar 1.15.0__py3-none-any.whl → 1.34.9__py3-none-any.whl

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 (129) hide show
  1. isar/__init__.py +2 -5
  2. isar/apis/api.py +159 -66
  3. isar/apis/models/__init__.py +0 -1
  4. isar/apis/models/models.py +22 -12
  5. isar/apis/models/start_mission_definition.py +128 -123
  6. isar/apis/robot_control/robot_controller.py +41 -0
  7. isar/apis/schedule/scheduling_controller.py +135 -121
  8. isar/apis/security/authentication.py +5 -5
  9. isar/config/certs/ca-cert.pem +32 -32
  10. isar/config/keyvault/keyvault_service.py +1 -2
  11. isar/config/log.py +47 -39
  12. isar/config/logging.conf +16 -31
  13. isar/config/open_telemetry.py +102 -0
  14. isar/config/predefined_mission_definition/default_exr.json +49 -0
  15. isar/config/predefined_mission_definition/default_mission.json +1 -5
  16. isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
  17. isar/config/predefined_missions/default.json +67 -87
  18. isar/config/predefined_missions/default_extra_capabilities.json +107 -0
  19. isar/config/settings.py +119 -142
  20. isar/eventhandlers/eventhandler.py +123 -0
  21. isar/mission_planner/local_planner.py +6 -20
  22. isar/mission_planner/mission_planner_interface.py +1 -1
  23. isar/models/events.py +184 -0
  24. isar/models/status.py +18 -0
  25. isar/modules.py +118 -205
  26. isar/robot/robot.py +377 -0
  27. isar/robot/robot_battery.py +60 -0
  28. isar/robot/robot_monitor_mission.py +357 -0
  29. isar/robot/robot_pause_mission.py +74 -0
  30. isar/robot/robot_resume_mission.py +67 -0
  31. isar/robot/robot_start_mission.py +66 -0
  32. isar/robot/robot_status.py +61 -0
  33. isar/robot/robot_stop_mission.py +68 -0
  34. isar/robot/robot_upload_inspection.py +75 -0
  35. isar/script.py +171 -0
  36. isar/services/service_connections/mqtt/mqtt_client.py +47 -11
  37. isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +32 -0
  38. isar/services/service_connections/mqtt/robot_info_publisher.py +4 -3
  39. isar/services/service_connections/persistent_memory.py +69 -0
  40. isar/services/utilities/mqtt_utilities.py +93 -0
  41. isar/services/utilities/robot_utilities.py +20 -0
  42. isar/services/utilities/scheduling_utilities.py +393 -65
  43. isar/state_machine/state_machine.py +227 -486
  44. isar/state_machine/states/__init__.py +0 -7
  45. isar/state_machine/states/await_next_mission.py +114 -0
  46. isar/state_machine/states/blocked_protective_stop.py +60 -0
  47. isar/state_machine/states/going_to_lockdown.py +95 -0
  48. isar/state_machine/states/going_to_recharging.py +92 -0
  49. isar/state_machine/states/home.py +115 -0
  50. isar/state_machine/states/intervention_needed.py +77 -0
  51. isar/state_machine/states/lockdown.py +38 -0
  52. isar/state_machine/states/maintenance.py +36 -0
  53. isar/state_machine/states/monitor.py +137 -166
  54. isar/state_machine/states/offline.py +60 -0
  55. isar/state_machine/states/paused.py +92 -23
  56. isar/state_machine/states/pausing.py +48 -0
  57. isar/state_machine/states/pausing_return_home.py +48 -0
  58. isar/state_machine/states/recharging.py +80 -0
  59. isar/state_machine/states/resuming.py +57 -0
  60. isar/state_machine/states/resuming_return_home.py +64 -0
  61. isar/state_machine/states/return_home_paused.py +109 -0
  62. isar/state_machine/states/returning_home.py +217 -0
  63. isar/state_machine/states/stopping.py +61 -0
  64. isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
  65. isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
  66. isar/state_machine/states/stopping_go_to_recharge.py +51 -0
  67. isar/state_machine/states/stopping_return_home.py +77 -0
  68. isar/state_machine/states/unknown_status.py +72 -0
  69. isar/state_machine/states_enum.py +22 -5
  70. isar/state_machine/transitions/mission.py +192 -0
  71. isar/state_machine/transitions/return_home.py +106 -0
  72. isar/state_machine/transitions/robot_status.py +80 -0
  73. isar/state_machine/utils/common_event_handlers.py +73 -0
  74. isar/storage/blob_storage.py +71 -45
  75. isar/storage/local_storage.py +28 -14
  76. isar/storage/storage_interface.py +28 -6
  77. isar/storage/uploader.py +184 -55
  78. isar/storage/utilities.py +35 -27
  79. isar-1.34.9.dist-info/METADATA +496 -0
  80. isar-1.34.9.dist-info/RECORD +135 -0
  81. {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
  82. isar-1.34.9.dist-info/entry_points.txt +3 -0
  83. robot_interface/models/exceptions/__init__.py +0 -7
  84. robot_interface/models/exceptions/robot_exceptions.py +274 -4
  85. robot_interface/models/initialize/__init__.py +0 -1
  86. robot_interface/models/inspection/__init__.py +0 -13
  87. robot_interface/models/inspection/inspection.py +43 -34
  88. robot_interface/models/mission/mission.py +18 -14
  89. robot_interface/models/mission/status.py +20 -25
  90. robot_interface/models/mission/task.py +156 -92
  91. robot_interface/models/robots/battery_state.py +6 -0
  92. robot_interface/models/robots/media.py +13 -0
  93. robot_interface/models/robots/robot_model.py +7 -7
  94. robot_interface/robot_interface.py +135 -66
  95. robot_interface/telemetry/mqtt_client.py +84 -12
  96. robot_interface/telemetry/payloads.py +111 -12
  97. robot_interface/utilities/json_service.py +7 -1
  98. isar/config/predefined_missions/default_turtlebot.json +0 -110
  99. isar/config/predefined_poses/__init__.py +0 -0
  100. isar/config/predefined_poses/predefined_poses.py +0 -616
  101. isar/config/settings.env +0 -26
  102. isar/mission_planner/sequential_task_selector.py +0 -23
  103. isar/mission_planner/task_selector_interface.py +0 -31
  104. isar/models/communication/__init__.py +0 -0
  105. isar/models/communication/message.py +0 -12
  106. isar/models/communication/queues/__init__.py +0 -4
  107. isar/models/communication/queues/queue_io.py +0 -12
  108. isar/models/communication/queues/queue_timeout_error.py +0 -2
  109. isar/models/communication/queues/queues.py +0 -19
  110. isar/models/communication/queues/status_queue.py +0 -20
  111. isar/models/mission_metadata/__init__.py +0 -0
  112. isar/services/readers/__init__.py +0 -0
  113. isar/services/readers/base_reader.py +0 -37
  114. isar/services/service_connections/mqtt/robot_status_publisher.py +0 -93
  115. isar/services/service_connections/stid/__init__.py +0 -0
  116. isar/services/service_connections/stid/stid_service.py +0 -45
  117. isar/services/utilities/queue_utilities.py +0 -39
  118. isar/state_machine/states/idle.py +0 -40
  119. isar/state_machine/states/initialize.py +0 -60
  120. isar/state_machine/states/initiate.py +0 -129
  121. isar/state_machine/states/off.py +0 -18
  122. isar/state_machine/states/stop.py +0 -78
  123. isar/storage/slimm_storage.py +0 -181
  124. isar-1.15.0.dist-info/METADATA +0 -417
  125. isar-1.15.0.dist-info/RECORD +0 -113
  126. robot_interface/models/initialize/initialize_params.py +0 -9
  127. robot_interface/models/mission/step.py +0 -211
  128. {isar-1.15.0.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
  129. {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
@@ -1,32 +1,30 @@
1
1
  import logging
2
2
  from http import HTTPStatus
3
- from typing import List, Optional
4
3
 
5
- from alitra import Pose
6
4
  from fastapi import Body, HTTPException, Path
7
- from injector import inject
5
+ from opentelemetry import trace
8
6
 
9
- from isar.apis.models import InputPose, StartMissionResponse
10
7
  from isar.apis.models.models import (
11
8
  ControlMissionResponse,
12
- RobotInfoResponse,
9
+ StartMissionResponse,
13
10
  TaskResponse,
14
11
  )
15
12
  from isar.apis.models.start_mission_definition import (
16
13
  StartMissionDefinition,
14
+ StopMissionDefinition,
17
15
  to_isar_mission,
18
16
  )
19
- from isar.config.settings import robot_settings, settings
17
+ from isar.config.settings import robot_settings
20
18
  from isar.mission_planner.mission_planner_interface import MissionPlannerError
21
19
  from isar.services.utilities.scheduling_utilities import SchedulingUtilities
22
20
  from isar.state_machine.states_enum import States
23
21
  from robot_interface.models.mission.mission import Mission
24
- from robot_interface.models.mission.step import DriveToPose, Localize
25
- from robot_interface.models.mission.task import Task
22
+ from robot_interface.models.mission.task import TASKS, InspectionTask
23
+
24
+ tracer = trace.get_tracer(__name__)
26
25
 
27
26
 
28
27
  class SchedulingController:
29
- @inject
30
28
  def __init__(
31
29
  self,
32
30
  scheduling_utilities: SchedulingUtilities,
@@ -34,54 +32,33 @@ class SchedulingController:
34
32
  self.scheduling_utilities: SchedulingUtilities = scheduling_utilities
35
33
  self.logger = logging.getLogger("api")
36
34
 
35
+ @tracer.start_as_current_span("start_mission_by_id")
37
36
  def start_mission_by_id(
38
37
  self,
39
38
  mission_id: str = Path(
40
- ...,
41
39
  alias="id",
42
40
  title="Mission ID",
43
41
  description="ID-number for predefined mission",
44
42
  ),
45
- initial_pose: Optional[InputPose] = Body(
46
- default=None,
47
- description="The starting point of the mission. Used for initial "
48
- "localization of robot",
49
- embed=True,
50
- ),
51
- return_pose: Optional[InputPose] = Body(
52
- default=None,
53
- description="End pose of the mission. The robot return to the specified "
54
- "pose after finishing all inspections",
55
- embed=True,
56
- ),
57
43
  ) -> StartMissionResponse:
58
- self.logger.info(f"Received request to start mission with id {mission_id}")
44
+ self.logger.info("Received request to start mission with id %s", mission_id)
59
45
 
60
46
  state: States = self.scheduling_utilities.get_state()
61
47
  self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
62
48
 
63
49
  mission: Mission = self.scheduling_utilities.get_mission(mission_id)
64
- if return_pose:
65
- pose: Pose = return_pose.to_alitra_pose()
66
- step: DriveToPose = DriveToPose(pose=pose)
67
- mission.tasks.append(Task(steps=[step]))
68
50
 
69
51
  self.scheduling_utilities.verify_robot_capable_of_mission(
70
52
  mission=mission, robot_capabilities=robot_settings.CAPABILITIES
71
53
  )
72
54
 
73
- initial_pose_alitra: Optional[Pose] = (
74
- initial_pose.to_alitra_pose() if initial_pose else None
75
- )
55
+ self.logger.info("Starting mission with ISAR Mission ID: '%s'", mission.id)
76
56
 
77
- self.logger.info(f"Starting mission with ISAR Mission ID: '{mission.id}'")
78
-
79
- self.scheduling_utilities.start_mission(
80
- mission=mission, initial_pose=initial_pose_alitra
81
- )
57
+ self.scheduling_utilities.start_mission(mission=mission)
82
58
 
83
59
  return self._api_response(mission)
84
60
 
61
+ @tracer.start_as_current_span("start_mission")
85
62
  def start_mission(
86
63
  self,
87
64
  mission_definition: StartMissionDefinition = Body(
@@ -90,35 +67,26 @@ class SchedulingController:
90
67
  title="Mission Definition",
91
68
  description="Description of the mission in json format",
92
69
  ),
93
- initial_pose: Optional[InputPose] = Body(
94
- default=None,
95
- description="The starting point of the mission. Used for initial "
96
- "localization of robot",
97
- embed=True,
98
- ),
99
- return_pose: Optional[InputPose] = Body(
100
- default=None,
101
- description="End pose of the mission. The robot return to the specified "
102
- "pose after finishing all inspections",
103
- embed=True,
104
- ),
105
70
  ) -> StartMissionResponse:
106
71
  self.logger.info("Received request to start new mission")
107
72
 
108
73
  if not mission_definition:
109
- error_message: str = (
74
+ error_message_no_mission_definition: str = (
110
75
  "Unprocessable entity - 'mission_definition' empty or invalid"
111
76
  )
112
- self.logger.error(error_message)
77
+ self.logger.error(error_message_no_mission_definition)
113
78
  raise HTTPException(
114
- status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=error_message
79
+ status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
80
+ detail=error_message_no_mission_definition,
115
81
  )
116
82
 
117
83
  state: States = self.scheduling_utilities.get_state()
118
84
  self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
119
85
 
120
86
  try:
121
- mission: Mission = to_isar_mission(mission_definition)
87
+ mission: Mission = to_isar_mission(
88
+ start_mission_definition=mission_definition
89
+ )
122
90
  except MissionPlannerError as e:
123
91
  error_message = f"Bad Request - Cannot create ISAR mission: {e}"
124
92
  self.logger.warning(error_message)
@@ -130,21 +98,23 @@ class SchedulingController:
130
98
  self.scheduling_utilities.verify_robot_capable_of_mission(
131
99
  mission=mission, robot_capabilities=robot_settings.CAPABILITIES
132
100
  )
133
- if return_pose:
134
- pose: Pose = return_pose.to_alitra_pose()
135
- step: DriveToPose = DriveToPose(pose=pose)
136
- mission.tasks.append(Task(steps=[step]))
137
101
 
138
- initial_pose_alitra: Optional[Pose] = (
139
- initial_pose.to_alitra_pose() if initial_pose else None
140
- )
102
+ self.logger.info("Starting mission: %s", mission.id)
103
+ self.scheduling_utilities.start_mission(mission=mission)
104
+ return self._api_response(mission)
105
+
106
+ @tracer.start_as_current_span("return_home")
107
+ def return_home(self) -> None:
108
+ self.logger.info("Received request to return home")
141
109
 
142
- self.logger.info(f"Starting mission: {mission.id}")
143
- self.scheduling_utilities.start_mission(
144
- mission=mission, initial_pose=initial_pose_alitra
110
+ state: States = self.scheduling_utilities.get_state()
111
+ self.scheduling_utilities.verify_state_machine_ready_to_receive_return_home_mission(
112
+ state
145
113
  )
146
- return self._api_response(mission)
147
114
 
115
+ self.scheduling_utilities.return_home()
116
+
117
+ @tracer.start_as_current_span("pause_mission")
148
118
  def pause_mission(self) -> ControlMissionResponse:
149
119
  self.logger.info("Received request to pause current mission")
150
120
 
@@ -152,7 +122,7 @@ class SchedulingController:
152
122
 
153
123
  if state not in [
154
124
  States.Monitor,
155
- States.Initiate,
125
+ States.ReturningHome,
156
126
  ]:
157
127
  error_message = (
158
128
  f"Conflict - Pause command received in invalid state - State: {state}"
@@ -168,12 +138,13 @@ class SchedulingController:
168
138
  )
169
139
  return pause_mission_response
170
140
 
141
+ @tracer.start_as_current_span("resume_mission")
171
142
  def resume_mission(self) -> ControlMissionResponse:
172
143
  self.logger.info("Received request to resume current mission")
173
144
 
174
145
  state: States = self.scheduling_utilities.get_state()
175
146
 
176
- if state != States.Paused:
147
+ if state not in [States.Paused, States.ReturnHomePaused]:
177
148
  error_message = (
178
149
  f"Conflict - Resume command received in invalid state - State: {state}"
179
150
  )
@@ -185,12 +156,29 @@ class SchedulingController:
185
156
  )
186
157
  return resume_mission_response
187
158
 
188
- def stop_mission(self) -> ControlMissionResponse:
159
+ @tracer.start_as_current_span("stop_mission")
160
+ def stop_mission(
161
+ self,
162
+ mission_id: StopMissionDefinition = Body(
163
+ default=None,
164
+ embed=True,
165
+ title="Mission ID to stop",
166
+ description="The mission ID of the mission being stopped, in json format",
167
+ ),
168
+ ) -> ControlMissionResponse:
169
+
189
170
  self.logger.info("Received request to stop current mission")
190
171
 
191
172
  state: States = self.scheduling_utilities.get_state()
192
173
 
193
- if state in [States.Off, States.Idle]:
174
+ if (
175
+ state == States.UnknownStatus
176
+ or state == States.Stopping
177
+ or state == States.BlockedProtectiveStop
178
+ or state == States.Offline
179
+ or state == States.Home
180
+ or state == States.ReturningHome
181
+ ):
194
182
  error_message = (
195
183
  f"Conflict - Stop command received in invalid state - State: {state}"
196
184
  )
@@ -198,71 +186,94 @@ class SchedulingController:
198
186
  raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
199
187
 
200
188
  stop_mission_response: ControlMissionResponse = (
201
- self.scheduling_utilities.stop_mission()
189
+ self.scheduling_utilities.stop_mission(mission_id.mission_id)
202
190
  )
203
191
  return stop_mission_response
204
192
 
205
- def drive_to(
206
- self,
207
- target_pose: InputPose = Body(
208
- default=None,
209
- title="Target Pose",
210
- description="The target pose for the drive_to step",
211
- ),
212
- ) -> StartMissionResponse:
213
- self.logger.info("Received request to start new drive-to mission")
193
+ @tracer.start_as_current_span("release_intervention_needed")
194
+ def release_intervention_needed(self) -> None:
195
+ self.logger.info("Received request to release intervention needed state")
214
196
 
215
197
  state: States = self.scheduling_utilities.get_state()
216
198
 
217
- self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
199
+ if state != States.InterventionNeeded:
200
+ error_message = f"Conflict - Release intervention needed command received in invalid state - State: {state}"
201
+ self.logger.warning(error_message)
202
+ raise HTTPException(
203
+ status_code=HTTPStatus.CONFLICT,
204
+ detail=error_message,
205
+ )
218
206
 
219
- pose: Pose = target_pose.to_alitra_pose()
220
- step: DriveToPose = DriveToPose(pose=pose)
221
- mission: Mission = Mission(tasks=[Task(steps=[step])])
207
+ self.scheduling_utilities.release_intervention_needed()
208
+ self.logger.info("Released intervention needed state successfully")
222
209
 
223
- self.logger.info(
224
- f"Starting drive to mission with ISAR Mission ID: '{mission.id}'"
225
- )
226
- self.scheduling_utilities.start_mission(mission=mission, initial_pose=None)
227
- return self._api_response(mission)
210
+ @tracer.start_as_current_span("lockdown")
211
+ def lockdown(self) -> None:
212
+ self.logger.info("Received request to lockdown robot")
228
213
 
229
- def start_localization_mission(
230
- self,
231
- localization_pose: InputPose = Body(
232
- default=None,
233
- embed=True,
234
- title="Localization Pose",
235
- description="The current position of the robot",
236
- ),
237
- ) -> StartMissionResponse:
238
- self.logger.info("Received request to start new localization mission")
214
+ state: States = self.scheduling_utilities.get_state()
215
+
216
+ if state == States.Lockdown:
217
+ error_message = "Conflict - Lockdown command received in lockdown state"
218
+ self.logger.warning(error_message)
219
+ raise HTTPException(
220
+ status_code=HTTPStatus.CONFLICT,
221
+ detail=error_message,
222
+ )
223
+
224
+ self.scheduling_utilities.lock_down_robot()
225
+ self.logger.info("Lockdown started successfully")
226
+
227
+ @tracer.start_as_current_span("release_lockdown")
228
+ def release_lockdown(self) -> None:
229
+ self.logger.info("Received request to release robot lockdown")
239
230
 
240
231
  state: States = self.scheduling_utilities.get_state()
241
232
 
242
- self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
233
+ if state != States.Lockdown:
234
+ error_message = f"Conflict - Release lockdown command received in invalid state - State: {state}"
235
+ self.logger.warning(error_message)
236
+ raise HTTPException(
237
+ status_code=HTTPStatus.CONFLICT,
238
+ detail=error_message,
239
+ )
243
240
 
244
- pose: Pose = localization_pose.to_alitra_pose()
245
- step: Localize = Localize(localization_pose=pose)
246
- mission: Mission = Mission(tasks=[Task(steps=[step])])
241
+ self.scheduling_utilities.release_robot_lockdown()
242
+ self.logger.info("Released lockdown successfully")
247
243
 
248
- self.logger.info(
249
- f"Starting localization mission with ISAR Mission ID: '{mission.id}'"
250
- )
251
- self.scheduling_utilities.start_mission(
252
- mission=mission,
253
- initial_pose=None,
254
- )
255
- return self._api_response(mission)
244
+ @tracer.start_as_current_span("maintenance_mode")
245
+ def set_maintenance_mode(self) -> None:
246
+ self.logger.info("Received request to set maintenance_mode")
256
247
 
257
- def get_info(self):
258
- return RobotInfoResponse(
259
- robot_package=settings.ROBOT_PACKAGE,
260
- isar_id=settings.ISAR_ID,
261
- robot_name=settings.ROBOT_NAME,
262
- robot_map_name=settings.DEFAULT_MAP,
263
- robot_capabilities=robot_settings.CAPABILITIES,
264
- plant_short_name=settings.STID_PLANT_NAME,
265
- )
248
+ state: States = self.scheduling_utilities.get_state()
249
+
250
+ if state == States.Maintenance or state == States.StoppingDueToMaintenance:
251
+ message = f"Conflict - Call to set maintenance mode was given while in state {state}."
252
+ self.logger.info(message)
253
+ raise HTTPException(
254
+ status_code=HTTPStatus.CONFLICT,
255
+ detail=message,
256
+ )
257
+
258
+ self.scheduling_utilities.set_maintenance_mode()
259
+ self.logger.info("Maintenance mode has been set")
260
+
261
+ @tracer.start_as_current_span("release_maintenance_mode")
262
+ def release_maintenance_mode(self) -> None:
263
+ self.logger.info("Received request to release robot from maintenance mode")
264
+
265
+ state: States = self.scheduling_utilities.get_state()
266
+
267
+ if state != States.Maintenance:
268
+ message = f"Conflict - Release maintenance mode command received in invalid state - State: {state}"
269
+ self.logger.info(message)
270
+ raise HTTPException(
271
+ status_code=HTTPStatus.CONFLICT,
272
+ detail=message,
273
+ )
274
+
275
+ self.scheduling_utilities.release_maintenance_mode()
276
+ self.logger.info("Maintenance mode successfully released")
266
277
 
267
278
  def _api_response(self, mission: Mission) -> StartMissionResponse:
268
279
  return StartMissionResponse(
@@ -270,9 +281,12 @@ class SchedulingController:
270
281
  tasks=[self._task_api_response(task) for task in mission.tasks],
271
282
  )
272
283
 
273
- def _task_api_response(self, task: Task) -> TaskResponse:
274
- steps: List[dict] = []
275
- for step in task.steps:
276
- steps.append({"id": step.id, "type": step.__class__.__name__})
284
+ def _task_api_response(self, task: TASKS) -> TaskResponse:
285
+ if isinstance(task, InspectionTask):
286
+ inspection_id = task.inspection_id
287
+ else:
288
+ inspection_id = None
277
289
 
278
- return TaskResponse(id=task.id, tag_id=task.tag_id, steps=steps)
290
+ return TaskResponse(
291
+ id=task.id, tag_id=task.tag_id, inspection_id=inspection_id, type=task.type
292
+ )
@@ -3,7 +3,7 @@ import logging
3
3
  from fastapi import Depends
4
4
  from fastapi.security.base import SecurityBase
5
5
  from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
6
- from fastapi_azure_auth.exceptions import InvalidAuth
6
+ from fastapi_azure_auth.exceptions import InvalidAuthHttp
7
7
  from fastapi_azure_auth.user import User
8
8
  from pydantic import BaseModel
9
9
 
@@ -35,7 +35,7 @@ async def validate_has_role(user: User = Depends(azure_scheme)) -> None:
35
35
  Raises a 403 authorization error if not.
36
36
  """
37
37
  if settings.REQUIRED_ROLE not in user.roles:
38
- raise InvalidAuth(
38
+ raise InvalidAuthHttp(
39
39
  "Current user does not possess the required role for this endpoint"
40
40
  )
41
41
 
@@ -48,9 +48,9 @@ class Authenticator:
48
48
  self.logger = logging.getLogger("api")
49
49
  self.authentication_enabled: bool = authentication_enabled
50
50
  enabled_string = "enabled" if self.authentication_enabled else "disabled"
51
- self.logger.info(f"API authentication is {enabled_string}")
51
+ self.logger.info("API authentication is %s", enabled_string)
52
52
 
53
- def should_authenticate(self):
53
+ def should_authenticate(self) -> bool:
54
54
  return self.authentication_enabled
55
55
 
56
56
  def get_scheme(self):
@@ -58,7 +58,7 @@ class Authenticator:
58
58
  return validate_has_role
59
59
  return NoSecurity
60
60
 
61
- async def load_config(self):
61
+ async def load_config(self) -> None:
62
62
  """
63
63
  Load OpenID config on startup.
64
64
  """
@@ -1,35 +1,35 @@
1
1
  -----BEGIN CERTIFICATE-----
2
- MIIGETCCA/mgAwIBAgIURlJMuiWItiyRuV8FbUKur8c4gJIwDQYJKoZIhvcNAQEL
3
- BQAwgZcxCzAJBgNVBAYTAk5PMQ8wDQYDVQQIDAZCZXJnZW4xDzANBgNVBAcMBkJl
2
+ MIIGGzCCBAOgAwIBAgIUCVuS8tL7R2bdjRJznkk1NN0oUa8wDQYJKoZIhvcNAQEL
3
+ BQAwgZwxCzAJBgNVBAYTAk5PMQ8wDQYDVQQIDAZCZXJnZW4xDzANBgNVBAcMBkJl
4
4
  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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- YD15gdP1vcQwY+/TOL7JR475m5vCMVmLRsFyZJcUVXWGTTpBbdqbf44off2dy1M8
19
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- N0ZaQNR+8uKtQhfdj8+gCUi82CRntStoOiVThGvF2MZOroerQh8IB7d8HLlIE6qZ
28
- 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
- 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
- 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
- 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
- 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
- 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
34
- QgqJDiX8Md2BtpClLiOZLuSnwlHC
5
+ GgYDVQQDDBNSb2JvdGljc19TZWxmU2lnbmVkMSQwIgYJKoZIhvcNAQkBFhVmZ19y
6
+ b2JvdHNAZXF1aW5vci5jb20wHhcNMjQxMjExMDczNzAzWhcNMjUxMjExMDczNzAz
7
+ WjCBnDELMAkGA1UEBhMCTk8xDzANBgNVBAgMBkJlcmdlbjEPMA0GA1UEBwwGQmVy
8
+ Z2VuMRQwEgYDVQQKDAtFcV9Sb2JvdGljczERMA8GA1UECwwIUm9ib3RpY3MxHDAa
9
+ BgNVBAMME1JvYm90aWNzX1NlbGZTaWduZWQxJDAiBgkqhkiG9w0BCQEWFWZnX3Jv
10
+ Ym90c0BlcXVpbm9yLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
11
+ ALVlT8EjA8UpXdARJM6YfhH+9DEyytb0SJYOEm9jWL7X0G5eMbNfKtfFj/BLQBBw
12
+ GDqyvhFa8SO56zM3Yi1Lir41JkpOKf2zJpNaL1syPxOA87MAm43UN/p2Mo6QAYpC
13
+ lJzmeNoagLFi/kIzGajeUwISOf//Zej0jK/9CpuvvNVHz4wRk4Ap9qtgrM9qhlNq
14
+ rZL4ZQODnvDZ/nwAYwEh9O2sbRr73aiHFcbxNAU2XxtT5UwbZMHom2Xic2Dmubqn
15
+ cWFU+C4upEqPvgYU/hxia/rkhHyicXmdXT6IzjVxPbrlPBHoKVodJOCQ3UoBQl5X
16
+ caHCluKOblA/yX+vEhv2h/m8XOtWiq0Jxnx+6D7B1piZfQnnNVSBT0bOYASb2cpn
17
+ 8rJE3RBdBwiqjKTotVTZ8PAS/GY0Gz3GoMAUCvIGA02+/cO9ENls9D79/86lgiCQ
18
+ UqyteNNmo+oMqKHBw9Jmeejw1kbpnJN1abc270+qCT17aeX8yiMza5CyUTQTtgFI
19
+ 62xIYaHWhN7GrxOvoMU8X0G1HINwjS5heIhu5bND/26dLZqV3aBUWwb4ql5QIce2
20
+ q5BQKmMqQLZF6J0aFL9dKJv5u3ioPFmiYPsPiRlwusPOoOFCiMt2kqZE3h3wxu6V
21
+ cXWdj0sH+dgxuGYTxrLuoSru4Xv/HPXh15mVY0/eI+u/AgMBAAGjUzBRMB0GA1Ud
22
+ DgQWBBQW2Ms6M3iadKVLOnraMlx6Exm8KjAfBgNVHSMEGDAWgBQW2Ms6M3iadKVL
23
+ OnraMlx6Exm8KjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQB0
24
+ XorfCLmVUN3aQWBouwG0fXHBqi3d4RA0vPsi8w+R/dOuHnLBLlHAiOkCMqW23bLs
25
+ ir2UVqpGXcp5keKGeCTj+bmdqhnS/B9eoaeA8np/zDPH834v+Qv7JA5ch50gYrL7
26
+ llF/02e5m0egP2xx9V7k90Du8OBm7xQgfbyrUcB2laC+loPyQiHMF24CU4ESfC4K
27
+ DBXOZm8Kd6+fvzEs8XDLKwjNb9MocHIJikysZTncxfhgVPaU5KCqF3afHkCkZGzn
28
+ Z+yMBfWbZ2WtM3lfwmM7z5NCTSqSFucW5+kw/OnzS3nQLyz2Q4nh4ApojsI3v+FH
29
+ xz8aCITPbVwKAHGYO9jbk9x8LlkzDPCCzBH7dGKLKaQSCoD+IOwig1cI7pM77WeI
30
+ BNC0g2ZCuLdb7c37/kT/1xGrIGUrpDsO34ZWW7E2X8kQPkAgqMNp0uXcnvIoNHO1
31
+ eVL08Om+7WbwK7wS3te5IWDq4rRWVW4Jv709w8z6porr3W8ipdtUoYqbgZI8jOiW
32
+ osY4UVcRwQ4S1oF5pNRzXlZAPwAnaDipglxHBiy9/ARS6HKpOCbmUHbj+goXYNyZ
33
+ ps8TcYc9LFOq4HBE4KKbV971jf/wQy1nVcm2qSbOJ9MPHM5BG22m1iVhSYbLPgE2
34
+ SBZvCS+voD8MqpBB1tPcvVqlPb1pFxf1lOc5QV3uOA==
35
35
  -----END CERTIFICATE-----
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import traceback
3
2
  from typing import Union
4
3
 
5
4
  from azure.core.exceptions import (
@@ -66,7 +65,7 @@ class Keyvault:
66
65
  raise KeyvaultError # type: ignore
67
66
 
68
67
  def get_secret_client(self) -> SecretClient:
69
- if self.client == None:
68
+ if self.client is None:
70
69
  try:
71
70
  credential: Union[ClientSecretCredential, DefaultAzureCredential]
72
71
  if self.client_id and self.client_secret and self.tenant_id:
isar/config/log.py CHANGED
@@ -1,63 +1,71 @@
1
- import importlib.resources as pkg_resources
2
1
  import logging
3
2
  import logging.config
3
+ import os
4
+ from importlib.resources import as_file, files
4
5
 
5
6
  import yaml
6
- from opencensus.ext.azure.log_exporter import AzureLogHandler
7
7
  from uvicorn.logging import ColourizedFormatter
8
8
 
9
- from isar.config.configuration_error import ConfigurationError
10
- from isar.config.keyvault.keyvault_error import KeyvaultError
11
- from isar.config.keyvault.keyvault_service import Keyvault
12
9
  from isar.config.settings import settings
13
10
 
11
+ from .settings import Settings
14
12
 
15
- def setup_loggers(keyvault: Keyvault) -> None:
16
- log_levels: dict = settings.LOG_LEVELS
17
- with pkg_resources.path("isar.config", "logging.conf") as path:
18
- log_config = yaml.safe_load(open(path))
13
+
14
+ def setup_loggers() -> None:
15
+ log_config = load_log_config()
19
16
 
20
17
  logging.config.dictConfig(log_config)
21
18
 
19
+ env_log_levels = {}
20
+ for env_var, value in os.environ.items():
21
+ if env_var.endswith("_LOG_LEVEL"):
22
+ log_name = env_var.split("_LOG_LEVEL")[0].lower()
23
+ env_log_levels[log_name] = value.upper()
24
+
22
25
  handlers = []
23
26
  if settings.LOG_HANDLER_LOCAL_ENABLED:
24
- handlers.append(configure_console_handler(log_config=log_config))
25
- if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
26
27
  handlers.append(
27
- configure_azure_handler(log_config=log_config, keyvault=keyvault)
28
+ configure_console_handler(log_config=log_config, settings=settings)
28
29
  )
29
30
 
30
31
  for log_handler in handlers:
31
- for loggers in log_config["loggers"].keys():
32
- logging.getLogger(loggers).addHandler(log_handler)
33
- logging.getLogger(loggers).setLevel(log_levels[loggers])
34
- logging.getLogger().addHandler(log_handler)
35
-
32
+ for logger_name, logger_config in log_config["loggers"].items():
33
+ logger = logging.getLogger(logger_name)
34
+ logger.addHandler(log_handler)
35
+ if "level" in logger_config:
36
+ logger.setLevel(logger_config["level"])
37
+ if logger_name in env_log_levels:
38
+ logger.setLevel(env_log_levels[logger_name])
39
+ root_logger = logging.getLogger()
40
+ root_logger.addHandler(log_handler)
41
+ if "level" in log_config.get("root", {}):
42
+ root_logger.setLevel(log_config["root"]["level"])
36
43
 
37
- def configure_console_handler(log_config: dict) -> logging.Handler:
38
- handler = logging.StreamHandler()
39
- handler.setLevel(log_config["root"]["level"])
40
- handler.setFormatter(
41
- ColourizedFormatter(
42
- log_config["formatters"]["colourized"]["format"],
43
- style="{",
44
- use_colors=True,
45
- )
46
- )
47
- return handler
48
44
 
45
+ def load_log_config():
46
+ source = files("isar").joinpath("config").joinpath("logging.conf")
47
+ with as_file(source) as f:
48
+ log_config = yaml.safe_load(open(f))
49
+ return log_config
49
50
 
50
- def configure_azure_handler(log_config: dict, keyvault: Keyvault) -> logging.Handler:
51
- connection_string: str
52
- try:
53
- connection_string = keyvault.get_secret(
54
- "application-insights-connection-string"
55
- ).value
56
- except KeyvaultError:
57
- message: str = f"CRITICAL ERROR: Missing connection string for Application Insights in key vault '{keyvault.name}'."
58
- print(f"\n{message} \n")
59
- raise ConfigurationError(message)
60
51
 
61
- handler = AzureLogHandler(connection_string=connection_string)
52
+ def configure_console_handler(log_config: dict, settings: Settings) -> logging.Handler:
53
+ handler = logging.StreamHandler()
62
54
  handler.setLevel(log_config["root"]["level"])
55
+ if settings.DEBUG_LOG_FORMATTER:
56
+ handler.setFormatter(
57
+ ColourizedFormatter(
58
+ log_config["formatters"]["debug-formatter"]["format"],
59
+ style="{",
60
+ use_colors=True,
61
+ )
62
+ )
63
+ else:
64
+ handler.setFormatter(
65
+ ColourizedFormatter(
66
+ log_config["formatters"]["colourized"]["format"],
67
+ style="{",
68
+ use_colors=True,
69
+ )
70
+ )
63
71
  return handler