From 9f3c6dbe7f6505743cbc26d277059b5263d324b3 Mon Sep 17 00:00:00 2001 From: Yuxing Fei Date: Tue, 30 Jul 2024 16:18:57 -0700 Subject: [PATCH] update page dependency --- .github/workflows/page.yaml | 2 +- alab_management/builders/samplebuilder.py | 15 ++- alab_management/device_manager.py | 6 +- alab_management/device_view/dbattributes.py | 31 +++-- alab_management/device_view/device.py | 31 +++-- alab_management/lab_view.py | 11 +- alab_management/logger.py | 3 + .../resource_manager/resource_requester.py | 35 +++--- alab_management/task_view/task_enums.py | 2 +- docs/source/{ => _static}/time-sensitive.png | Bin docs/source/advance_topics.md | 10 ++ docs/source/best_practices.md | 111 ++++++++++++------ docs/source/index.rst | 3 +- docs/source/installation.rst | 4 +- docs/source/submit_experiments.md | 2 +- docs/source/test.md | 63 +++++----- pyproject.toml | 3 +- 17 files changed, 197 insertions(+), 135 deletions(-) rename docs/source/{ => _static}/time-sensitive.png (100%) create mode 100644 docs/source/advance_topics.md diff --git a/.github/workflows/page.yaml b/.github/workflows/page.yaml index 0a37dc68..421be43c 100644 --- a/.github/workflows/page.yaml +++ b/.github/workflows/page.yaml @@ -28,7 +28,7 @@ jobs: python -m sphinx -T -E -b html -d _build/doctrees -D language=en ./docs/source _build/html - name: Deploy uses: peaceiris/actions-gh-pages@v3 - if: ${{ github.ref == 'refs/heads/master' }} + if: ${{ github.ref == 'refs/heads/main' }} with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: _build/html diff --git a/alab_management/builders/samplebuilder.py b/alab_management/builders/samplebuilder.py index 929cf887..08a436ab 100644 --- a/alab_management/builders/samplebuilder.py +++ b/alab_management/builders/samplebuilder.py @@ -53,12 +53,15 @@ def to_dict(self) -> dict[str, Any]: """Return Sample as a dictionary. This looks like: - { - "_id": str(ObjectId), - "name": str, - "tags": List[str], - "metadata": Dict[str, Any], - } + + .. code-block:: + + { + "_id": str(ObjectId), + "name": str, + "tags": List[str], + "metadata": Dict[str, Any], + } Returns ------- diff --git a/alab_management/device_manager.py b/alab_management/device_manager.py index 6cf8050b..d196121b 100644 --- a/alab_management/device_manager.py +++ b/alab_management/device_manager.py @@ -324,14 +324,14 @@ def create_device_wrapper( def call(self, device_name: str, method: str, *args, **kwargs) -> Any: """ - Call a method inside the device with name ``device_name``. *args, **kwargs will be feeded into + Call a method inside the device with name ``device_name``. args, kwargs will be feeded into the method directly. Args: device_name: the name of device, which is defined by administer. method: the class method to call - *args: positional arguments to feed into the method function - **kwargs: keyword arguments to feed into the method function + args: positional arguments to feed into the method function + kwargs: keyword arguments to feed into the method function Returns ------- diff --git a/alab_management/device_view/dbattributes.py b/alab_management/device_view/dbattributes.py index 418c076c..634e3857 100644 --- a/alab_management/device_view/dbattributes.py +++ b/alab_management/device_view/dbattributes.py @@ -21,28 +21,27 @@ def value_in_database(name: str, default_value: Any) -> property: Example usage when defining a new Device: .. code-block:: python - from alab_management.device_view import BaseDevice, value_in_database - class MyDevice(BaseDevice): - my_attribute = value_in_database("my_attribute", 0) + from alab_management.device_view import BaseDevice, value_in_database - def __init__(self, name: str, **kwargs): - super().__init__(name, **kwargs) - self.name = name - self.my_attribute #initial call to the property, which sets the default value in the database + class MyDevice(BaseDevice): + my_attribute = value_in_database("my_attribute", 0) - .... - #first instantiation + def __init__(self, name: str, **kwargs): + super().__init__(name, **kwargs) + self.name = name + self.my_attribute #initial call to the property, which sets the default value in the database - mydevice = MyDevice(name = "mydevice_1") - mydevice.my_attribute = 5 #sets the value in the database - - .... - #future instantiation - mydevice = MyDevice(name = "mydevice_1") - mydevice.my_attribute #retrieves value from db and returns 5 + .... + #first instantiation + mydevice = MyDevice(name = "mydevice_1") + mydevice.my_attribute = 5 #sets the value in the database + .... + #future instantiation + mydevice = MyDevice(name = "mydevice_1") + mydevice.my_attribute #retrieves value from db and returns 5 """ def getter(self) -> Any: diff --git a/alab_management/device_view/device.py b/alab_management/device_view/device.py index 63590eed..94f7f40e 100644 --- a/alab_management/device_view/device.py +++ b/alab_management/device_view/device.py @@ -202,7 +202,8 @@ def __init__(self, address: str, port: int = 502, *args, **kwargs): @property @abstractmethod def description(self) -> str: - """A short description of the device. This will be stored in the database + displayed in the dashboard. This + """ + A short description of the device. This will be stored in the database + displayed in the dashboard. This must be declared in subclasses of BaseDevice!. """ return self._description @@ -380,18 +381,21 @@ def request_maintenance(self, prompt: str, options: list[Any]): def retrieve_signal( self, signal_name: str, within: datetime.timedelta | None = None ): - """Retrieve a signal from the database. - - Args: signal_name (str): device signal name. This should match the signal_name passed to the - `@log_device_signal` decorator within (Optional[datetime.timedelta], optional): timedelta defining how far - back to pull logs from (relative to current time). Defaults to None. - - Returns ------- Dict: Dictionary of signal result. Single value vs lists depends on whether `within` was None - or not, respectively. Form is: { "device_name": "device_name", "signal_name": "signal_name", - "value": "signal_value" or ["signal_value_1", "signal_value_2", ...]], "timestamp": "timestamp" or [ - "timestamp_1", "timestamp_2", ...] } + """ + Retrieve a signal from the database. + Args: + signal_name (str): device signal name. This should match the signal_name passed to the + ``@log_device_signal`` decorator + within (Optional[datetime.timedelta], optional): + timedelta defining how far back to pull logs from (relative to current time). Defaults to None. + Returns + ------- + Dict: Dictionary of signal result. Single value vs lists depends on whether ``within`` was None + or not, respectively. Form is: { "device_name": "device_name", "signal_name": "signal_name", + "value": "signal_value" or ["signal_value_1", "signal_value_2", ...]], "timestamp": "timestamp" or [ + "timestamp_1", "timestamp_2", ...] } """ return self._signalemitter.retrieve_signal(signal_name, within) @@ -441,11 +445,12 @@ def __init__(self, device: BaseDevice): def get_methods_to_log(self): """ - Log the data from all methods decorated with `@log_signal` to the database. + Log the data from all methods decorated with ``@log_signal`` to the database. - Collected all the methods that are decorated with `@log_signal` and return a dictionary of the form: + Collected all the methods that are decorated with ``@log_signal`` and return a dictionary of the form: .. code-block:: + { : { "interval": , diff --git a/alab_management/lab_view.py b/alab_management/lab_view.py index fb64629f..b9414f64 100644 --- a/alab_management/lab_view.py +++ b/alab_management/lab_view.py @@ -200,13 +200,14 @@ def update_sample_metadata(self, sample: ObjectId | str, metadata: dict[str, Any def run_subtask( self, task: type[BaseTask], samples: list[ObjectId | str], **kwargs ): - """Run a task as a subtask within the task. basically fills in task_id and lab_view for you. - this command blocks until the subtask is completed. + """ + Run a task as a subtask within the task. basically fills in task_id and lab_view for you. + this command blocks until the subtask is completed. Args: - task (Type[BaseTask]): The type/class of the Task to run. - samples (List[Union[ObjectId, str]]): List of sample IDs or names. - **kwargs: will be passed to the Task method via the parameters entry in the task collection. + task (Type[BaseTask]): The type/class of the Task to run. + samples (List[Union[ObjectId, str]]): List of sample IDs or names. + **kwargs: will be passed to the Task method via the parameters entry in the task collection. """ if not issubclass(task, BaseTask): raise TypeError("task must be a subclass of BaseTask!") diff --git a/alab_management/logger.py b/alab_management/logger.py index 01b97f34..f67ac2f2 100644 --- a/alab_management/logger.py +++ b/alab_management/logger.py @@ -132,6 +132,9 @@ def get_latest_device_signal( Optional[Any]: dictionary with result. dict example: + + .. code-block:: + { "device_name": device_name, "signal_name": signal_name, diff --git a/alab_management/resource_manager/resource_requester.py b/alab_management/resource_manager/resource_requester.py index 0be1a0b1..bdbcf71f 100644 --- a/alab_management/resource_manager/resource_requester.py +++ b/alab_management/resource_manager/resource_requester.py @@ -59,22 +59,25 @@ class ResourceRequestItem(BaseModel): class ResourcesRequest(RootModel): """ This class is used to validate the resource request. Each request should have a format of - [ - { - "device":{ - "identifier": "name" or "type" or "nodevice", - "content": string corresponding to identifier - }, - "sample_positions": [ - { - "prefix": prefix of sample position, - "number": integer number of such positions requested. - }, - ... - ] - }, - ... - ]. + + .. code-block:: + + [ + { + "device":{ + "identifier": "name" or "type" or "nodevice", + "content": string corresponding to identifier + }, + "sample_positions": [ + { + "prefix": prefix of sample position, + "number": integer number of such positions requested. + }, + ... + ] + }, + ... + ]. See Also -------- diff --git a/alab_management/task_view/task_enums.py b/alab_management/task_view/task_enums.py index 31a827e7..e5a90836 100644 --- a/alab_management/task_view/task_enums.py +++ b/alab_management/task_view/task_enums.py @@ -47,7 +47,7 @@ class CancelingProgress(Enum): PENDING: The canceling process has been initiated. WORKER_NOTIFIED: The worker has been notified to cancel the request, which means an - abort error has been raised in the worker. + abort error has been raised in the worker. """ PENDING = auto() diff --git a/docs/source/time-sensitive.png b/docs/source/_static/time-sensitive.png similarity index 100% rename from docs/source/time-sensitive.png rename to docs/source/_static/time-sensitive.png diff --git a/docs/source/advance_topics.md b/docs/source/advance_topics.md new file mode 100644 index 00000000..b4c7e815 --- /dev/null +++ b/docs/source/advance_topics.md @@ -0,0 +1,10 @@ +# Advance Topics + +In this section, we will discuss some of the more advanced topics that can help you better set up the lab. + +```{toctree} +:maxdepth: 1 +:hidden: + +test +``` \ No newline at end of file diff --git a/docs/source/best_practices.md b/docs/source/best_practices.md index 8e0d7999..6b847c37 100644 --- a/docs/source/best_practices.md +++ b/docs/source/best_practices.md @@ -1,43 +1,57 @@ # Best Practices -To ease the deployment of AlabOS, below are several example solutions to common problems or challenges found during implementation: + +To ease the deployment of AlabOS, below are several example solutions to common problems or challenges found during +implementation: ## 1. Minimizing duration for time-sensitive samples -`Solution`: To ensure the time-sensitive samples are processed with minimum duration, one can first reserve all devices before running the first process that starts the timer for the sample. + +`Solution`: To ensure the time-sensitive samples are processed with minimum duration, one can first reserve all devices +before running the first process that starts the timer for the sample. + ### Example system: -Take this scenario for `solid_wet_mixing_and_pipetting` task. The objective is to mix solids with ethanol and pipette the slurry into another container. The sample starts as unmixed solids dispensed inside a polypropylene mixing pot with pressence of zirconia balls (to transfer energy during mixing) and ends as slurry inside an alumina crucible container as illustrated in the figure below. -![alt text](time-sensitive.png) -For this process, the timer starts when the first ethanol drop hits the solid mix. The challenge present in this system is that the solid mix can densify if given enough time under ethanol pressence. Therefore, the series of process `ethanol_dispensing`, `mixing`, and `slurry_pipetting` have to be done as fast as possible for each sample. To do this, one have to ensure that all device are always available for the sample whenever the sample needs it. +Take this scenario for `solid_wet_mixing_and_pipetting` task. The objective is to mix solids with ethanol and pipette +the slurry into another container. The sample starts as unmixed solids dispensed inside a polypropylene mixing pot with +pressence of zirconia balls (to transfer energy during mixing) and ends as slurry inside an alumina crucible container +as illustrated in the figure below. +![Time Sensitive Operation](_static/time-sensitive.png) + +For this process, the timer starts when the first ethanol drop hits the solid mix. The challenge present in this system +is that the solid mix can densify if given enough time under ethanol pressence. Therefore, the series of +process `ethanol_dispensing`, `mixing`, and `slurry_pipetting` have to be done as fast as possible for each sample. To +do this, one have to ensure that all device are always available for the sample whenever the sample needs it. + ### Solution implementation for the system: + ```python class Solid_Wet_Mixing_and_Pipetting(BaseTask): def __init__( - self, - ethanol_amount: float = 5000, # in ul, 5 mL by default - mixing_duration: float = 600, # in seconds, 10 minutes by default - *args, - **kwargs, + self, + ethanol_amount: float = 5000, # in ul, 5 mL by default + mixing_duration: float = 600, # in seconds, 10 minutes by default + *args, + **kwargs, ): priority = kwargs.pop("priority", TaskPriority.HIGH) super().__init__(priority=priority, *args, **kwargs) - self.ethanol_amount=ethanol_amount - self.mixing_duration=mixing_duration + self.ethanol_amount = ethanol_amount + self.mixing_duration = mixing_duration def run(): - sample=self.samples[0] + sample = self.samples[0] with self.lab_view.request_resources({ - IndexingQuadrant:{"crucible/slot":1}, - EthanolDispenser:{}, - Mixer:{}, - SlurryPipette:{}, - RobotArm:{}, - None: { - "slurry_transfer_crucible_position": 1 - } - }) as ( - devices, - sample_positions, - ): + IndexingQuadrant: {"crucible/slot": 1}, + EthanolDispenser: {}, + Mixer: {}, + SlurryPipette: {}, + RobotArm: {}, + None: { + "slurry_transfer_crucible_position": 1 + } + }) as ( + devices, + sample_positions, + ): indexing_quadrant: IndexingQuadrant = devices[IndexingQuadrant] ethanol_dispenser: EthanolDispenser = devices[EthanolDispenser] mixer: Mixer = devices[Mixer] @@ -45,39 +59,60 @@ class Solid_Wet_Mixing_and_Pipetting(BaseTask): robot_arm: RobotArm = devices[RobotArm] initial_position = self.lab_view.get_sample(sample=self.sample).position destination = list(sample_positions[EthanolDispenser]["slot"])[0] - robot_arm.move(sample,destination) + robot_arm.move(sample, destination) ethanol_dispenser.dispense(self.ethanol_amount) destination = list(sample_positions[Mixer]["slot"])[0] - robot_arm.move(sample,destination) + robot_arm.move(sample, destination) mixer.mix(self.mixing_duration) destination = list(sample_positions[SlurryPipette]["slot"])[0] - robot_arm.move(sample,destination) + robot_arm.move(sample, destination) slurry_pipette.transfer(self.ethanol_amount) # move back empty mixing pot to rack destination = initial_position - robot_arm.move(sample,destination) + robot_arm.move(sample, destination) # consider sample to be only in the crucible now self.lab_view.move_sample( sample=sample, position=positions[None]["powdertransfer_crucible_position"][0], ) - destination=list(sample_positions[IndexingQuadrant]["crucible/slot"])[0] - robot_arm.move(sample,destination) + destination = list(sample_positions[IndexingQuadrant]["crucible/slot"])[0] + robot_arm.move(sample, destination) ``` -In this solution, before running any of the time-sensitive process, all the devices and sample positions involved in the process is booked. Then, the robot begins the series of processes until it finishes everything, ensuring minimum time is achieved for this specific sample. The next sample will run once this specific sample is done. Note that all resources are available because the booking ensures the devices and sample positions are exclusively available for this task. + +In this solution, before running any of the time-sensitive process, all the devices and sample positions involved in the +process is booked. Then, the robot begins the series of processes until it finishes everything, ensuring minimum time is +achieved for this specific sample. The next sample will run once this specific sample is done. Note that all resources +are available because the booking ensures the devices and sample positions are exclusively available for this task. ## 2. Sharing device/instrument between automated workflow and manual usage -Oftentimes due to scarcity of availability of some device/instrument, an automated system has to pause a part of its operation and allow human experimentalist to use the instrument. + +Oftentimes due to scarcity of availability of some device/instrument, an automated system has to pause a part of its +operation and allow human experimentalist to use the instrument. ### Case study: SEM -Let's take a look at the following case study for a scanning electron microscope (SEM) instrument that is fully integrated into an autonomous laboratory running on AlabOS. The SEM is placed such that it can be fully operated by human, given the robots that usually loads the samples into the SEM do not interact with it while the human operator is using it. -User X wants to use the SEM under manual mode because their sample requires a special scanning parameters and steps not implemented yet in the automated laboratory. They want to manually load their sample because it comes in an irregular shape. Essentially, they want to use the SEM for a certain duration. Note that the person also do not want to interrupt any work that is being done on the other samples inside the autonomous workflow. +Let's take a look at the following case study for a scanning electron microscope (SEM) instrument that is fully +integrated into an autonomous laboratory running on AlabOS. The SEM is placed such that it can be fully operated by +human, given the robots that usually loads the samples into the SEM do not interact with it while the human operator is +using it. + +User X wants to use the SEM under manual mode because their sample requires a special scanning parameters and steps not +implemented yet in the automated laboratory. They want to manually load their sample because it comes in an irregular +shape. Essentially, they want to use the SEM for a certain duration. Note that the person also do not want to interrupt +any work that is being done on the other samples inside the autonomous workflow. ### Solution: Pausing SEM -User X can just click "Pause" in the AlabOS user interface to request pause to the running task that is currently operating on the samples inside the SEM. Once the task can be interrupted gracefully, AlabOS will pause the device and not run any task requiring such device. This includes pausing SEM and any robots that interacts with SEM because in the automatic SEM sample preparation, loading, data collection, and analysis, all the corresponding devices are booked in advance before running the tasks. If any task is still running, AlabOS will keep the device in "requesting pause" state. Hence, User X can wait until the SEM status is "Paused" and then directly use the SEM, worry-free, as long as they return the SEM back to one of the expected states according to the automated SEM program/task. -## 3. Cancelling samples and removing them out of the automated workflow gracefully +User X can just click "Pause" in the AlabOS user interface to request pause to the running task that is currently +operating on the samples inside the SEM. Once the task can be interrupted gracefully, AlabOS will pause the device and +not run any task requiring such device. This includes pausing SEM and any robots that interacts with SEM because in the +automatic SEM sample preparation, loading, data collection, and analysis, all the corresponding devices are booked in +advance before running the tasks. If any task is still running, AlabOS will keep the device in "requesting pause" state. +Hence, User X can wait until the SEM status is "Paused" and then directly use the SEM, worry-free, as long as they +return the SEM back to one of the expected states according to the automated SEM program/task. -TODO +## 3. Cancelling samples and removing them out of the automated workflow gracefully +```{note} +More content will be added soon. +``` diff --git a/docs/source/index.rst b/docs/source/index.rst index 70b2a622..7d9e99e1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -106,7 +106,8 @@ via `pymongo `_ package. installation tutorial - bestpractice + best_practices + advance_topics Development API Docs diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 4d2ba7cc..c348f943 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -8,12 +8,12 @@ MongoDB ~~~~~~~ You must have access to at least one `MongoDB database `_ (locally or remotely). -To install MongoDB locally, refer to `this `_. +To install MongoDB locally, refer to `MongoDB website `_. RabbitMQ ~~~~~~~~ -You must have RabbitMQ installed on your machine. To install RabbitMQ, refer to `this `_. +You must have RabbitMQ installed on your machine. To install RabbitMQ, refer to `Rabbitmq website `_. Install via pip ---------------- diff --git a/docs/source/submit_experiments.md b/docs/source/submit_experiments.md index 7ada2e00..177cebcb 100644 --- a/docs/source/submit_experiments.md +++ b/docs/source/submit_experiments.md @@ -191,7 +191,7 @@ while True: ``` The format of the status is as follows: -```json +``` { "id": "experiment_id", "name": "experiment_name", diff --git a/docs/source/test.md b/docs/source/test.md index 10be412d..6fdfe49d 100644 --- a/docs/source/test.md +++ b/docs/source/test.md @@ -1,19 +1,22 @@ -### To ensure a seamless installation and robust testing of the software system, several key processes have -been implemented: +# Testing for Alab Definition -1. Testing Framework: - - We have implemented a suite of unit tests using the pytest framework. These tests cover + +To ensure a seamless installation and robust testing of the software system, several key processes have been implemented. + +1. Testing Framework + - a suite of unit tests using the pytest framework. These tests cover all functionalities of tasks and devices, ensuring that each component works as expected in isolation. - In addition to unit tests, integration tests are also developed. These tests validate the interactions between different components of the system. Integration tests are simulating real-world scenarios which help us to ensure that the system works correctly as a whole. -2. Continuous Integration (CI): +2. Continuous Integration (CI) - A CI pipeline is set up using tools like GitHub Actions. This pipeline automatically runs the entire test suite (both unit and integration tests) whenever new code is committed. This ensures that any changes introduced do not break existing functionalities. -Methodology: + +## Methodology Before writing the unit tests, one needs to modify the files under devices & tasks by adding the ``@mock decorator`` to all the places that require connections to all the hardware in Alab. @@ -37,37 +40,35 @@ Once all the methods that talk to alab_control are mocked, we can now move on to - **Step 6**: The unittests for tasks are also similar. They have certain pytest fixtures that can be used by the downstream unittests. These unit tests provide a set of dummy config, path and input files and checks whether the expected outcomes are met. -### Example: +## Example -## Device Class: BoxFurnace: +### Device Class: BoxFurnace Key functionalities of the BoxFurnace class: - - initialization: Sets up communication port, driver, door controller, and furnace letter. - - connect/disconnect: Manages connection to the furnace driver. - - run_program: Executes heating programs with specified parameters or profiles. - - emergent_stop: Immediately stops the furnace operation. - - get_temperature: Retrieves the current temperature. - - open_door/close_door: Manages the furnace door operations. - - is_running: Checks if the furnace is currently running a program. - -## Unit Tests for BoxFurnace: + - `initialization`: Sets up communication port, driver, door controller, and furnace letter. + - `connect/disconnect`: Manages connection to the furnace driver. + - `run_program`: Executes heating programs with specified parameters or profiles. + - `emergent_stop`: Immediately stops the furnace operation. + - `get_temperature`: Retrieves the current temperature. + - `open_door/close_door`: Manages the furnace door operations. + - `is_running`: Checks if the furnace is currently running a program. -## Fixtures +#### Fixtures Fixtures set up the necessary preconditions for the tests: - - door_controller_ab and door_controller_cd: Create instances of DoorController + - `door_controller_ab` and `door_controller_cd`: Create instances of DoorController for different sets of furnaces. - - box_furnace: Parameterized fixture to create BoxFurnace instances with various + - `box_furnace`: Parameterized fixture to create BoxFurnace instances with various configurations. - - mock_drivers: Mocks the furnace and door controller drivers to isolate and test + - `mock_drivers`: Mocks the furnace and door controller drivers to isolate and test BoxFurnace methods without real hardware. -## Tests +#### Tests 1. Basic Connection Tests: - - test_connect: Verifies that the connect method correctly initializes the + - `test_connect`: Verifies that the connect method correctly initializes the furnace and door controller drivers. - - test_disconnect: Ensures that the disconnect method properly sets the + - `test_disconnect:` Ensures that the disconnect method properly sets the furnace driver to None. ```python @@ -83,9 +84,9 @@ def test_disconnect(box_furnace, mock_drivers): ``` 2. Functional Tests: - - test_sample_positions: Checks the sample_positions property to ensure it + - `test_sample_positions`: Checks the sample_positions property to ensure it returns expected slot details. - - test_emergent_stop: Confirms that the emergent_stop method calls the + - `test_emergent_stop`: Confirms that the emergent_stop method calls the driver’s stop function. ```python @@ -103,17 +104,17 @@ def test_sample_positions(box_furnace): ``` 3. Program Execution Tests: - - test_run_program: Validates the run_program method for specified heating + - `test_run_program`: Validates the run_program method for specified heating times and temperatures, ensuring segments are correctly constructed and sent to the driver. - - test_run_program_with_profiles: Tests running custom heating profiles to + - `test_run_program_with_profiles`: Tests running custom heating profiles to verify segment creation and driver invocation. 4. Utility Tests: - - test_is_running: Checks if the is_running method correctly returns the + - `test_is_running`: Checks if the is_running method correctly returns the furnace’s running state based on environment variables. - - test_get_temperature: Ensures the get_temperature method fetches the + - `test_get_temperature`: Ensures the get_temperature method fetches the current temperature from the driver. - - test_open_door and test_close_door: Verify that the open_door and close_door methods invoke + - `test_open_door` and `test_close_door`: Verify that the open_door and close_door methods invoke - the door controller’s methods with the correct parameters. diff --git a/pyproject.toml b/pyproject.toml index e5a14929..7acf4811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,8 @@ dev = [ "requests >= 2.26.0", "flake8-bugbear >= 21.11.29", "flake8-docstrings >= 1.6.0", - "ruff" + "ruff", + "dramatiq[rabbitmq]==1.16.0" ] tests = ["pytest-cov==4.1.0", "pytest==7.4.1", "moto==4.2.2", "pytest-env ~= 0.6.2"] vis = ["matplotlib", "pydot"]