opentrons 8.3.2a0__py2.py3-none-any.whl → 8.4.0__py2.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 (196) hide show
  1. opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
  2. opentrons/calibration_storage/ot2/tip_length.py +6 -6
  3. opentrons/config/advanced_settings.py +9 -11
  4. opentrons/config/feature_flags.py +0 -4
  5. opentrons/config/reset.py +7 -2
  6. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  7. opentrons/drivers/asyncio/communication/async_serial.py +4 -0
  8. opentrons/drivers/asyncio/communication/errors.py +41 -8
  9. opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
  10. opentrons/drivers/flex_stacker/__init__.py +9 -3
  11. opentrons/drivers/flex_stacker/abstract.py +140 -15
  12. opentrons/drivers/flex_stacker/driver.py +593 -47
  13. opentrons/drivers/flex_stacker/errors.py +64 -0
  14. opentrons/drivers/flex_stacker/simulator.py +222 -24
  15. opentrons/drivers/flex_stacker/types.py +211 -15
  16. opentrons/drivers/flex_stacker/utils.py +19 -0
  17. opentrons/execute.py +4 -2
  18. opentrons/hardware_control/api.py +5 -0
  19. opentrons/hardware_control/backends/flex_protocol.py +4 -0
  20. opentrons/hardware_control/backends/ot3controller.py +12 -1
  21. opentrons/hardware_control/backends/ot3simulator.py +3 -0
  22. opentrons/hardware_control/backends/subsystem_manager.py +8 -4
  23. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
  24. opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
  25. opentrons/hardware_control/modules/__init__.py +12 -1
  26. opentrons/hardware_control/modules/absorbance_reader.py +11 -9
  27. opentrons/hardware_control/modules/flex_stacker.py +498 -0
  28. opentrons/hardware_control/modules/heater_shaker.py +12 -10
  29. opentrons/hardware_control/modules/magdeck.py +5 -1
  30. opentrons/hardware_control/modules/tempdeck.py +5 -1
  31. opentrons/hardware_control/modules/thermocycler.py +15 -14
  32. opentrons/hardware_control/modules/types.py +191 -1
  33. opentrons/hardware_control/modules/utils.py +3 -0
  34. opentrons/hardware_control/motion_utilities.py +20 -0
  35. opentrons/hardware_control/ot3api.py +145 -15
  36. opentrons/hardware_control/protocols/liquid_handler.py +47 -1
  37. opentrons/hardware_control/types.py +6 -0
  38. opentrons/legacy_commands/commands.py +102 -5
  39. opentrons/legacy_commands/helpers.py +74 -1
  40. opentrons/legacy_commands/types.py +33 -2
  41. opentrons/protocol_api/__init__.py +2 -0
  42. opentrons/protocol_api/_liquid.py +39 -8
  43. opentrons/protocol_api/_liquid_properties.py +20 -19
  44. opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
  45. opentrons/protocol_api/core/common.py +3 -1
  46. opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
  47. opentrons/protocol_api/core/engine/instrument.py +1356 -107
  48. opentrons/protocol_api/core/engine/labware.py +8 -4
  49. opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
  50. opentrons/protocol_api/core/engine/module_core.py +118 -2
  51. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +6 -14
  52. opentrons/protocol_api/core/engine/protocol.py +253 -11
  53. opentrons/protocol_api/core/engine/stringify.py +19 -8
  54. opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
  55. opentrons/protocol_api/core/engine/well.py +73 -5
  56. opentrons/protocol_api/core/instrument.py +71 -21
  57. opentrons/protocol_api/core/labware.py +6 -2
  58. opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
  59. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
  60. opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
  61. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
  62. opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
  63. opentrons/protocol_api/core/legacy/load_info.py +4 -12
  64. opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
  65. opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
  66. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
  67. opentrons/protocol_api/core/module.py +43 -0
  68. opentrons/protocol_api/core/protocol.py +33 -0
  69. opentrons/protocol_api/core/well.py +23 -2
  70. opentrons/protocol_api/instrument_context.py +454 -150
  71. opentrons/protocol_api/labware.py +98 -50
  72. opentrons/protocol_api/module_contexts.py +140 -0
  73. opentrons/protocol_api/protocol_context.py +163 -19
  74. opentrons/protocol_api/validation.py +51 -41
  75. opentrons/protocol_engine/__init__.py +21 -2
  76. opentrons/protocol_engine/actions/actions.py +5 -5
  77. opentrons/protocol_engine/clients/sync_client.py +6 -0
  78. opentrons/protocol_engine/commands/__init__.py +66 -36
  79. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
  80. opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
  81. opentrons/protocol_engine/commands/aspirate.py +6 -2
  82. opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
  83. opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
  84. opentrons/protocol_engine/commands/blow_out.py +2 -0
  85. opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
  86. opentrons/protocol_engine/commands/command_unions.py +102 -33
  87. opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
  88. opentrons/protocol_engine/commands/dispense.py +3 -1
  89. opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
  90. opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
  91. opentrons/protocol_engine/commands/drop_tip.py +23 -1
  92. opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
  93. opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
  94. opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
  95. opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
  96. opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
  97. opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
  98. opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
  99. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
  100. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
  101. opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
  102. opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
  103. opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
  104. opentrons/protocol_engine/commands/liquid_probe.py +27 -13
  105. opentrons/protocol_engine/commands/load_labware.py +42 -39
  106. opentrons/protocol_engine/commands/load_lid.py +21 -13
  107. opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
  108. opentrons/protocol_engine/commands/load_module.py +18 -17
  109. opentrons/protocol_engine/commands/load_pipette.py +3 -0
  110. opentrons/protocol_engine/commands/move_labware.py +139 -20
  111. opentrons/protocol_engine/commands/move_to_well.py +5 -11
  112. opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
  113. opentrons/protocol_engine/commands/pipetting_common.py +159 -8
  114. opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
  115. opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
  116. opentrons/protocol_engine/commands/reload_labware.py +6 -19
  117. opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
  118. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
  119. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
  120. opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
  121. opentrons/protocol_engine/errors/__init__.py +10 -0
  122. opentrons/protocol_engine/errors/exceptions.py +62 -0
  123. opentrons/protocol_engine/execution/equipment.py +123 -106
  124. opentrons/protocol_engine/execution/labware_movement.py +8 -6
  125. opentrons/protocol_engine/execution/pipetting.py +235 -25
  126. opentrons/protocol_engine/execution/tip_handler.py +82 -32
  127. opentrons/protocol_engine/labware_offset_standardization.py +194 -0
  128. opentrons/protocol_engine/protocol_engine.py +22 -13
  129. opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
  130. opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
  131. opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
  132. opentrons/protocol_engine/resources/labware_validation.py +7 -5
  133. opentrons/protocol_engine/slot_standardization.py +11 -23
  134. opentrons/protocol_engine/state/addressable_areas.py +84 -46
  135. opentrons/protocol_engine/state/frustum_helpers.py +36 -14
  136. opentrons/protocol_engine/state/geometry.py +892 -227
  137. opentrons/protocol_engine/state/labware.py +252 -55
  138. opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
  139. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
  140. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
  141. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
  142. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
  143. opentrons/protocol_engine/state/modules.py +210 -67
  144. opentrons/protocol_engine/state/pipettes.py +54 -0
  145. opentrons/protocol_engine/state/state.py +1 -1
  146. opentrons/protocol_engine/state/tips.py +14 -0
  147. opentrons/protocol_engine/state/update_types.py +180 -25
  148. opentrons/protocol_engine/state/wells.py +55 -9
  149. opentrons/protocol_engine/types/__init__.py +300 -0
  150. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  151. opentrons/protocol_engine/types/command_annotations.py +53 -0
  152. opentrons/protocol_engine/types/deck_configuration.py +72 -0
  153. opentrons/protocol_engine/types/execution.py +96 -0
  154. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  155. opentrons/protocol_engine/types/instrument.py +47 -0
  156. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  157. opentrons/protocol_engine/types/labware.py +111 -0
  158. opentrons/protocol_engine/types/labware_movement.py +22 -0
  159. opentrons/protocol_engine/types/labware_offset_location.py +111 -0
  160. opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
  161. opentrons/protocol_engine/types/liquid.py +40 -0
  162. opentrons/protocol_engine/types/liquid_class.py +59 -0
  163. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  164. opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
  165. opentrons/protocol_engine/types/location.py +194 -0
  166. opentrons/protocol_engine/types/module.py +301 -0
  167. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  168. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  169. opentrons/protocol_engine/types/tip.py +18 -0
  170. opentrons/protocol_engine/types/util.py +21 -0
  171. opentrons/protocol_engine/types/well_position.py +124 -0
  172. opentrons/protocol_reader/extract_labware_definitions.py +7 -3
  173. opentrons/protocol_reader/file_format_validator.py +5 -3
  174. opentrons/protocol_runner/json_translator.py +4 -2
  175. opentrons/protocol_runner/legacy_command_mapper.py +6 -2
  176. opentrons/protocol_runner/run_orchestrator.py +4 -1
  177. opentrons/protocols/advanced_control/transfers/common.py +48 -1
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/instrument.py +16 -3
  181. opentrons/protocols/labware.py +27 -23
  182. opentrons/protocols/models/__init__.py +0 -21
  183. opentrons/simulate.py +4 -2
  184. opentrons/types.py +20 -7
  185. opentrons/util/logging_config.py +94 -25
  186. opentrons/util/logging_queue_handler.py +61 -0
  187. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
  188. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
  189. opentrons/calibration_storage/ot2/models/defaults.py +0 -0
  190. opentrons/calibration_storage/ot3/models/defaults.py +0 -0
  191. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  192. opentrons/protocol_engine/types.py +0 -1311
  193. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
  194. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
  195. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
  196. {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ import logging
4
4
  import json
5
5
  import os
6
6
  from pathlib import Path
7
- from typing import Any, AnyStr, Dict, Optional, Union, List, Sequence, Literal
7
+ from typing import Mapping, Optional, Union, List, Sequence, Literal
8
8
 
9
9
  import jsonschema # type: ignore
10
10
 
@@ -46,8 +46,8 @@ def get_labware_definition(
46
46
  load_name: str,
47
47
  namespace: Optional[str] = None,
48
48
  version: Optional[int] = None,
49
- bundled_defs: Optional[Dict[str, LabwareDefinition]] = None,
50
- extra_defs: Optional[Dict[str, LabwareDefinition]] = None,
49
+ bundled_defs: Optional[Mapping[str, LabwareDefinition]] = None,
50
+ extra_defs: Optional[Mapping[str, LabwareDefinition]] = None,
51
51
  ) -> LabwareDefinition:
52
52
  """
53
53
  Look up and return a definition by load_name + namespace + version and
@@ -147,51 +147,55 @@ def save_definition(
147
147
 
148
148
 
149
149
  def verify_definition( # noqa: C901
150
- contents: Union[AnyStr, LabwareDefinition, Dict[str, Any]]
150
+ contents: str | bytes | LabwareDefinition | object,
151
151
  ) -> LabwareDefinition:
152
152
  """Verify that an input string is a labware definition and return it.
153
153
 
154
- If the definition is invalid, an exception is raised; otherwise parse the
155
- json and return the valid definition.
154
+ :param contents: The untrusted input to parse and validate. If str or bytes, it's
155
+ parsed as JSON. Otherwise, it should be the output of json.load().
156
156
 
157
- :raises json.JsonDecodeError: If the definition is not valid json
158
- :raises jsonschema.ValidationError: If the definition is not valid.
159
- :returns: The parsed definition
157
+ :raises NotALabwareError:
158
+
159
+ :returns: The parsed and validated definition
160
160
  """
161
161
  schemata_by_version = {
162
162
  2: json.loads(load_shared_data("labware/schemas/2.json").decode("utf-8")),
163
163
  3: json.loads(load_shared_data("labware/schemas/3.json").decode("utf-8")),
164
164
  }
165
165
 
166
- if isinstance(contents, dict):
167
- to_return = contents
168
- else:
169
- try:
170
- to_return = json.loads(contents)
171
- except json.JSONDecodeError as e:
172
- raise NotALabwareError("invalid-json", [e]) from e
173
166
  try:
174
- schema_version = to_return["schemaVersion"]
175
- except KeyError as e:
176
- raise NotALabwareError("no-schema-id", [e]) from e
167
+ parsed_json: object = (
168
+ json.loads(contents) if isinstance(contents, (str, bytes)) else contents
169
+ )
170
+ except json.JSONDecodeError as e:
171
+ raise NotALabwareError("invalid-json", [e]) from e
172
+
173
+ if isinstance(parsed_json, dict):
174
+ try:
175
+ schema_version: object = parsed_json["schemaVersion"]
176
+ except KeyError as e:
177
+ raise NotALabwareError("no-schema-id", [e]) from e
178
+ else:
179
+ raise NotALabwareError("no-schema-id", [])
177
180
 
178
181
  try:
179
- schema = schemata_by_version[schema_version]
182
+ # we can type ignore this because we handle the KeyError below
183
+ schema = schemata_by_version[schema_version] # type: ignore[index]
180
184
  except KeyError as e:
181
185
  raise NotALabwareError("bad-schema-id", [e]) from e
182
186
 
183
187
  try:
184
- jsonschema.validate(to_return, schema)
188
+ jsonschema.validate(parsed_json, schema)
185
189
  except jsonschema.ValidationError as e:
186
190
  raise NotALabwareError("schema-mismatch", [e]) from e
187
191
 
188
192
  # we can type ignore this because if it passes the jsonschema it has
189
193
  # the correct structure
190
- return to_return # type: ignore[return-value]
194
+ return parsed_json # type: ignore[return-value]
191
195
 
192
196
 
193
197
  def _get_labware_definition_from_bundle(
194
- bundled_labware: Dict[str, LabwareDefinition],
198
+ bundled_labware: Mapping[str, LabwareDefinition],
195
199
  load_name: str,
196
200
  namespace: Optional[str] = None,
197
201
  version: Optional[int] = None,
@@ -1,21 +0,0 @@
1
- # Convenience re-exports of models that are especially common or important.
2
- # More detailed sub-models are always available through the underlying
3
- # submodules.
4
- #
5
- # If re-exporting something, its name should still make sense when it's separated
6
- # from the name of its parent submodule. e.g. re-exporting models.json_protocol.Labware
7
- # as models.Labware could be confusing.
8
-
9
- # TODO(mc, 2022-03-11): remove this re-export when it won't break pickling
10
- # https://opentrons.atlassian.net/browse/RSS-94
11
- from opentrons_shared_data.labware.labware_definition import (
12
- LabwareDefinition,
13
- WellDefinition,
14
- )
15
- from .json_protocol import Model as JsonProtocol
16
-
17
- __all__ = [
18
- "LabwareDefinition",
19
- "WellDefinition",
20
- "JsonProtocol",
21
- ]
opentrons/simulate.py CHANGED
@@ -71,7 +71,9 @@ from opentrons.protocols.api_support.deck_type import (
71
71
  should_load_fixed_trash_labware_for_python_protocol,
72
72
  )
73
73
  from opentrons.protocols.api_support.types import APIVersion
74
- from opentrons_shared_data.labware.labware_definition import LabwareDefinition
74
+ from opentrons_shared_data.labware.labware_definition import (
75
+ labware_definition_type_adapter,
76
+ )
75
77
 
76
78
  from .util import entrypoint_util
77
79
 
@@ -829,7 +831,7 @@ def _create_live_context_pe(
829
831
  # Non-async would use call_soon_threadsafe(), which makes the waiting harder.
830
832
  async def add_all_extra_labware() -> None:
831
833
  for labware_definition_dict in extra_labware.values():
832
- labware_definition = LabwareDefinition.model_validate(
834
+ labware_definition = labware_definition_type_adapter.validate_python(
833
835
  labware_definition_dict
834
836
  )
835
837
  pe.add_labware_definition(labware_definition)
opentrons/types.py CHANGED
@@ -88,6 +88,15 @@ LocationLabware = Union[
88
88
  ]
89
89
 
90
90
 
91
+ class MeniscusTrackingTarget(enum.Enum):
92
+ START = "start"
93
+ END = "end"
94
+ DYNAMIC = "dynamic"
95
+
96
+ def __str__(self) -> str:
97
+ return self.name
98
+
99
+
91
100
  class Location:
92
101
  """Location(point: Point, labware: Union["Labware", "Well", str, "ModuleGeometry", LabwareLike, None, "ModuleContext"])
93
102
 
@@ -129,12 +138,12 @@ class Location:
129
138
  "ModuleContext",
130
139
  ],
131
140
  *,
132
- _ot_internal_is_meniscus: Optional[bool] = None,
141
+ _meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
133
142
  ):
134
143
  self._point = point
135
144
  self._given_labware = labware
136
145
  self._labware = LabwareLike(labware)
137
- self._is_meniscus = _ot_internal_is_meniscus
146
+ self._meniscus_tracking = _meniscus_tracking
138
147
 
139
148
  # todo(mm, 2021-10-01): Figure out how to get .point and .labware to show up
140
149
  # in the rendered docs, and then update the class docstring to use cross-references.
@@ -148,8 +157,8 @@ class Location:
148
157
  return self._labware
149
158
 
150
159
  @property
151
- def is_meniscus(self) -> Optional[bool]:
152
- return self._is_meniscus
160
+ def meniscus_tracking(self) -> Optional[MeniscusTrackingTarget]:
161
+ return self._meniscus_tracking
153
162
 
154
163
  def __iter__(self) -> Iterator[Union[Point, LabwareLike]]:
155
164
  """Iterable interface to support unpacking. Like a tuple.
@@ -167,7 +176,7 @@ class Location:
167
176
  isinstance(other, Location)
168
177
  and other._point == self._point
169
178
  and other._labware == self._labware
170
- and other._is_meniscus == self._is_meniscus
179
+ and other._meniscus_tracking == self._meniscus_tracking
171
180
  )
172
181
 
173
182
  def move(self, point: Point) -> "Location":
@@ -190,10 +199,14 @@ class Location:
190
199
 
191
200
  """
192
201
 
193
- return Location(point=self.point + point, labware=self._given_labware)
202
+ return Location(
203
+ point=self.point + point,
204
+ labware=self._given_labware,
205
+ _meniscus_tracking=self._meniscus_tracking,
206
+ )
194
207
 
195
208
  def __repr__(self) -> str:
196
- return f"Location(point={repr(self._point)}, labware={self._labware}, is_meniscus={self._is_meniscus if self._is_meniscus is not None else False})"
209
+ return f"Location(point={repr(self._point)}, labware={self._labware}, meniscus_tracking={self._meniscus_tracking})"
197
210
 
198
211
 
199
212
  # TODO(mc, 2020-10-22): use MountType implementation for Mount
@@ -1,7 +1,8 @@
1
1
  import logging
2
2
  from logging.config import dictConfig
3
+ from logging.handlers import QueueListener, RotatingFileHandler
3
4
  import sys
4
- from typing import Any, Dict
5
+ from queue import Queue
5
6
 
6
7
  from opentrons.config import CONFIG, ARCHITECTURE, SystemArchitecture
7
8
 
@@ -12,11 +13,33 @@ else:
12
13
  SENSOR_LOG_NAME = "unused"
13
14
 
14
15
 
15
- def _host_config(level_value: int) -> Dict[str, Any]:
16
+ # We want this big enough to smooth over any temporary stalls in journald's ability
17
+ # to consume our records--but bounded, so if we consistently outpace journald for
18
+ # some reason, we don't leak memory or get latency from buffer bloat.
19
+ # 50000 is basically an arbitrary guess.
20
+ _LOG_QUEUE_SIZE = 50000
21
+
22
+
23
+ log_queue = Queue[logging.LogRecord](maxsize=_LOG_QUEUE_SIZE)
24
+ """A buffer through which log records will pass.
25
+
26
+ This is intended to work around problems when our logs are going to journald:
27
+ we think journald can block for a while when it flushes records to the filesystem,
28
+ and the backpressure from that will cause calls like `log.debug()` to block and
29
+ interfere with timing-sensitive hardware control.
30
+ https://github.com/Opentrons/opentrons/issues/18034
31
+
32
+ `log_init()` will configure all the logs that this package knows about to pass through
33
+ this queue. This queue is exposed so consumers of this package (i.e. robot-server)
34
+ can do the same thing with their own logs, which is important to preserve ordering.
35
+ """
36
+
37
+
38
+ def _config_for_host(level_value: int) -> None:
16
39
  serial_log_filename = CONFIG["serial_log_file"]
17
40
  api_log_filename = CONFIG["api_log_file"]
18
41
  sensor_log_filename = CONFIG["sensor_log_file"]
19
- return {
42
+ config = {
20
43
  "version": 1,
21
44
  "disable_existing_loggers": False,
22
45
  "formatters": {
@@ -90,13 +113,20 @@ def _host_config(level_value: int) -> Dict[str, Any]:
90
113
  },
91
114
  }
92
115
 
116
+ dictConfig(config)
93
117
 
94
- def _buildroot_config(level_value: int) -> Dict[str, Any]:
118
+
119
+ def _config_for_robot(level_value: int) -> None:
95
120
  # Import systemd.journald here since it is generally unavailble on non
96
121
  # linux systems and we probably don't want to use it on linux desktops
97
122
  # either
123
+ from systemd.journal import JournalHandler # type: ignore
124
+
98
125
  sensor_log_filename = CONFIG["sensor_log_file"]
99
- return {
126
+
127
+ sensor_log_queue = Queue[logging.LogRecord](maxsize=_LOG_QUEUE_SIZE)
128
+
129
+ config = {
100
130
  "version": 1,
101
131
  "disable_existing_loggers": False,
102
132
  "formatters": {
@@ -104,36 +134,38 @@ def _buildroot_config(level_value: int) -> Dict[str, Any]:
104
134
  },
105
135
  "handlers": {
106
136
  "api": {
107
- "class": "systemd.journal.JournalHandler",
137
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
108
138
  "level": logging.DEBUG,
109
139
  "formatter": "message_only",
110
- "SYSLOG_IDENTIFIER": "opentrons-api",
140
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api"},
141
+ "queue": log_queue,
111
142
  },
112
143
  "serial": {
113
- "class": "systemd.journal.JournalHandler",
144
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
114
145
  "level": logging.DEBUG,
115
146
  "formatter": "message_only",
116
- "SYSLOG_IDENTIFIER": "opentrons-api-serial",
147
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api-serial"},
148
+ "queue": log_queue,
117
149
  },
118
150
  "can_serial": {
119
- "class": "systemd.journal.JournalHandler",
151
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
120
152
  "level": logging.DEBUG,
121
153
  "formatter": "message_only",
122
- "SYSLOG_IDENTIFIER": "opentrons-api-serial-can",
154
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api-serial-can"},
155
+ "queue": log_queue,
123
156
  },
124
157
  "usbbin_serial": {
125
- "class": "systemd.journal.JournalHandler",
158
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
126
159
  "level": logging.DEBUG,
127
160
  "formatter": "message_only",
128
- "SYSLOG_IDENTIFIER": "opentrons-api-serial-usbbin",
161
+ "extra": {"SYSLOG_IDENTIFIER": "opentrons-api-serial-usbbin"},
162
+ "queue": log_queue,
129
163
  },
130
164
  "sensor": {
131
- "class": "logging.handlers.RotatingFileHandler",
132
- "formatter": "message_only",
133
- "filename": sensor_log_filename,
134
- "maxBytes": 1000000,
165
+ "class": "opentrons.util.logging_queue_handler.CustomQueueHandler",
135
166
  "level": logging.DEBUG,
136
- "backupCount": 3,
167
+ "formatter": "message_only",
168
+ "queue": sensor_log_queue,
137
169
  },
138
170
  },
139
171
  "loggers": {
@@ -169,12 +201,47 @@ def _buildroot_config(level_value: int) -> Dict[str, Any]:
169
201
  },
170
202
  }
171
203
 
204
+ # Start draining from the queue and sending messages to journald.
205
+ # Then, stash the queue listener in a global variable so it doesn't get garbage-collected.
206
+ # I don't know if we actually need to do this, but let's not find out the hard way.
207
+ global _queue_listener
208
+ if _queue_listener is not None:
209
+ # In case this log init function was called multiple times for some reason.
210
+ _queue_listener.stop()
211
+ _queue_listener = QueueListener(log_queue, JournalHandler())
212
+ _queue_listener.start()
213
+
214
+ # Sensor logs are a special one-off thing that go to their own file instead of journald.
215
+ # We apply the same QueueListener performance workaround for basically the same reasons.
216
+ sensor_rotating_file_handler = RotatingFileHandler(
217
+ filename=sensor_log_filename, maxBytes=1000000, backupCount=3
218
+ )
219
+ sensor_rotating_file_handler.setLevel(logging.DEBUG)
220
+ sensor_rotating_file_handler.setFormatter(logging.Formatter(fmt="%(message)s"))
221
+ global _sensor_queue_listener
222
+ if _sensor_queue_listener is not None:
223
+ _sensor_queue_listener.stop()
224
+ _sensor_queue_listener = QueueListener(
225
+ sensor_log_queue, sensor_rotating_file_handler
226
+ )
227
+ _sensor_queue_listener.start()
228
+
229
+ dictConfig(config)
172
230
 
173
- def _config(arch: SystemArchitecture, level_value: int) -> Dict[str, Any]:
174
- return {
175
- SystemArchitecture.YOCTO: _buildroot_config,
176
- SystemArchitecture.BUILDROOT: _buildroot_config,
177
- SystemArchitecture.HOST: _host_config,
231
+ # TODO(2025-04-15): We need some kind of log_deinit() function to call
232
+ # queue_listener.stop() before the process ends. Not doing that means we're
233
+ # dropping some records when the process shuts down.
234
+
235
+
236
+ _queue_listener: QueueListener | None = None
237
+ _sensor_queue_listener: QueueListener | None = None
238
+
239
+
240
+ def _config(arch: SystemArchitecture, level_value: int) -> None:
241
+ {
242
+ SystemArchitecture.YOCTO: _config_for_robot,
243
+ SystemArchitecture.BUILDROOT: _config_for_robot,
244
+ SystemArchitecture.HOST: _config_for_host,
178
245
  }[arch](level_value)
179
246
 
180
247
 
@@ -191,6 +258,8 @@ def log_init(level_name: str) -> None:
191
258
  f"Defaulting to {fallback_log_level}\n"
192
259
  )
193
260
  ot_log_level = fallback_log_level
261
+
262
+ # todo(mm, 2025-04-14): Use logging.getLevelNamesMapping() when we have Python >=3.11.
194
263
  level_value = logging._nameToLevel[ot_log_level]
195
- logging_config = _config(ARCHITECTURE, level_value)
196
- dictConfig(logging_config)
264
+
265
+ _config(ARCHITECTURE, level_value)
@@ -0,0 +1,61 @@
1
+ # noqa: D100
2
+
3
+
4
+ import logging.handlers
5
+ import logging
6
+ from queue import Queue
7
+ from typing import cast
8
+ from typing_extensions import override
9
+
10
+
11
+ class CustomQueueHandler(logging.handlers.QueueHandler):
12
+ """A logging.QueueHandler with some customizations.
13
+
14
+ - Allow adding `extra` data to handled log records.
15
+
16
+ - Simplify and optimize for single-process use.
17
+
18
+ - If a new message comes in but the queue is full, block until it has room.
19
+ (The default QueueHandler drops records in a way we probably wouldn't notice.)
20
+ """
21
+
22
+ def __init__(
23
+ self, *, queue: Queue[logging.LogRecord], extra: dict[str, object] | None = None
24
+ ) -> None:
25
+ """Construct the handler.
26
+
27
+ Args:
28
+ queue: When this handler receives a log record, it will insert the message
29
+ into this queue.
30
+ extra: Extra data to attach to each log record, to be interpreted by
31
+ whatever handler is on the consuming side of the queue. e.g. if that's
32
+ `systemd.journal.JournalHandler`, you could add a "SYSLOG_IDENTIFIER"
33
+ key here. This corresponds to the `extra` arg of `Logger.debug()`.
34
+ """
35
+ super().__init__(queue=queue)
36
+
37
+ # Double underscore because we're subclassing external code so we should try to
38
+ # avoid collisions with its attributes.
39
+ self.__extra = extra
40
+
41
+ @override
42
+ def prepare(self, record: logging.LogRecord) -> logging.LogRecord:
43
+ """Called internally by the superclass before enqueueing a record."""
44
+ if self.__extra is not None:
45
+ # This looks questionable, but updating __dict__ is the documented behavior
46
+ # of `Logger.debug(msg, extra=...)`.
47
+ record.__dict__.update(self.__extra)
48
+
49
+ # We intentionally do *not* call `super().prepare(record)`. It's documented to
50
+ # muck with the data in the LogRecord, apparently as part of supporting
51
+ # inter-process use. Since we don't need that, we can preserve the original
52
+ # data and also save some compute time.
53
+ return record
54
+
55
+ @override
56
+ def enqueue(self, record: logging.LogRecord) -> None:
57
+ """Called internally by the superclass to enqueue a record."""
58
+ # This cast is safe because we constrain the type of `self.queue`
59
+ # in our `__init__()` and nobody should mutate it after-the-fact, in practice.
60
+ queue = cast(Queue[logging.LogRecord], self.queue)
61
+ queue.put(record)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentrons
3
- Version: 8.3.2a0
3
+ Version: 8.4.0
4
4
  Summary: The Opentrons API is a simple framework designed to make writing automated biology lab protocols easy.
5
5
  Author: Opentrons
6
6
  Author-email: engineering@opentrons.com
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Topic :: Scientific/Engineering
22
22
  Requires-Python: >=3.10
23
23
  License-File: ../LICENSE
24
- Requires-Dist: opentrons-shared-data (==8.3.2a0)
24
+ Requires-Dist: opentrons-shared-data (==8.4.0)
25
25
  Requires-Dist: aionotify (==0.3.1)
26
26
  Requires-Dist: anyio (<4.0.0,>=3.6.1)
27
27
  Requires-Dist: jsonschema (<4.18.0,>=3.0.1)
@@ -35,9 +35,9 @@ Requires-Dist: pyusb (==1.2.1)
35
35
  Requires-Dist: packaging (>=21.0)
36
36
  Requires-Dist: importlib-metadata (>=1.0) ; python_version < "3.8"
37
37
  Provides-Extra: flex-hardware
38
- Requires-Dist: opentrons-hardware[flex] (==8.3.2a0) ; extra == 'flex-hardware'
38
+ Requires-Dist: opentrons-hardware[flex] (==8.4.0) ; extra == 'flex-hardware'
39
39
  Provides-Extra: ot2-hardware
40
- Requires-Dist: opentrons-hardware (==8.3.2a0) ; extra == 'ot2-hardware'
40
+ Requires-Dist: opentrons-hardware (==8.4.0) ; extra == 'ot2-hardware'
41
41
 
42
42
  .. _Full API Documentation: http://docs.opentrons.com
43
43