11"""File interaction resource provider."""
22from datetime import datetime
3+ from enum import Enum
4+ from io import StringIO
5+ import csv
36from typing import List , Optional , Callable , Awaitable , Dict
47from pydantic import BaseModel
58from ..errors import StorageLimitReachedError
69
710
8- MAXIMUM_CSV_FILE_LIMIT = 400
11+ MAXIMUM_FILE_LIMIT = 400
912
1013
11- class GenericCsvTransform :
12- """Generic CSV File Type data for rows of data to be seperated by a delimeter ."""
14+ class MimeType ( str , Enum ) :
15+ """File mime types ."""
1316
14- filename : str
15- rows : List [List [str ]]
16- delimiter : str = ","
17+ TEXT_CSV = "text/csv"
18+
19+
20+ class ReadCmdFileNameMetadata (BaseModel ):
21+ """Data from a plate reader `read` command used to build the finalized file name."""
22+
23+ base_filename : str
24+ wavelength : int
25+
26+
27+ CommandFileNameMetadata = ReadCmdFileNameMetadata | None
28+
29+
30+ class FileData :
31+ """File data container for writing to a file."""
32+
33+ data : bytes
34+ mime_type : MimeType
35+ command_metadata : CommandFileNameMetadata
1736
1837 @staticmethod
1938 def build (
20- filename : str , rows : List [List [str ]], delimiter : str = ","
21- ) -> "GenericCsvTransform" :
22- """Build a Generic CSV datatype class."""
23- if "." in filename and not filename .endswith (".csv" ):
24- raise ValueError (
25- f"Provided filename { filename } invalid. Only CSV file format is accepted."
26- )
27- elif "." not in filename :
28- filename = f"{ filename } .csv"
29- csv = GenericCsvTransform ()
30- csv .filename = filename
31- csv .rows = rows
32- csv .delimiter = delimiter
33- return csv
39+ data : bytes ,
40+ mime_type : MimeType ,
41+ command_metadata : CommandFileNameMetadata = None ,
42+ ) -> "FileData" :
43+ """Build a generic file data class."""
44+ file_data = FileData ()
45+ file_data .data = data
46+ file_data .mime_type = mime_type
47+ file_data .command_metadata = command_metadata
48+ return file_data
3449
3550
3651class ReadData (BaseModel ):
@@ -41,29 +56,24 @@ class ReadData(BaseModel):
4156
4257
4358class PlateReaderData (BaseModel ):
44- """Data from a Opentrons Plate Reader Read. Can be converted to CSV template format."""
59+ """Data from an Opentrons Plate Reader Read. Can be converted to CSV format."""
4560
4661 read_results : List [ReadData ]
4762 reference_wavelength : Optional [int ] = None
4863 start_time : datetime
4964 finish_time : datetime
5065 serial_number : str
5166
52- def build_generic_csv ( # noqa: C901
53- self , filename : str , measurement : ReadData
54- ) -> GenericCsvTransform :
55- """Builds a CSV compatible object containing Plate Reader Measurements.
56-
57- This will also automatically reformat the provided filename to include the wavelength of those measurements.
58- """
67+ def build_csv_bytes (self , measurement : ReadData ) -> bytes : # noqa: C901
68+ """Builds CSV data as bytes containing Plate Reader Measurements."""
5969 plate_alpharows = ["A" , "B" , "C" , "D" , "E" , "F" , "G" , "H" ]
6070 rows = []
6171
6272 rows .append (["" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" , "11" , "12" ])
6373 for i in range (8 ):
6474 row = [plate_alpharows [i ]]
6575 for j in range (12 ):
66- row .append (str (measurement .data [f"{ plate_alpharows [i ]} { j + 1 } " ]))
76+ row .append (str (measurement .data [f"{ plate_alpharows [i ]} { j + 1 } " ]))
6777 rows .append (row )
6878 for i in range (3 ):
6979 rows .append ([])
@@ -116,46 +126,50 @@ def build_generic_csv( # noqa: C901
116126 ["Measurement finished at" , self .finish_time .strftime ("%m %d %H:%M:%S %Y" )]
117127 )
118128
119- # Ensure the filename adheres to ruleset contains the wavelength for a given measurement
120- if filename . endswith ( ".csv" ):
121- filename = filename [: - 4 ]
122- filename = filename + str ( measurement . wavelength ) + "nm.csv"
129+ output = StringIO ()
130+ writer = csv . writer ( output , delimiter = "," )
131+ writer . writerows ( rows )
132+ csv_bytes = output . getvalue (). encode ( "utf-8" )
123133
124- return GenericCsvTransform .build (
125- filename = filename ,
126- rows = rows ,
127- delimiter = "," ,
128- )
134+ return csv_bytes
129135
130136
131137class FileProvider :
132138 """Provider class to wrap file read write interactions to the data files directory in the engine."""
133139
134140 def __init__ (
135141 self ,
136- data_files_write_csv_callback : Optional [
137- Callable [[GenericCsvTransform ], Awaitable [str ]]
138- ] = None ,
142+ data_files_write_file_cb : Optional [Callable [[FileData ], Awaitable [str ]]] = None ,
139143 data_files_filecount : Optional [Callable [[], Awaitable [int ]]] = None ,
140144 ) -> None :
141145 """Initialize the interface callbacks of the File Provider for data file handling within the Protocol Engine.
142146
143147 Params:
144- data_files_write_csv_callback : Callback to write a CSV file to the data files directory and add it to the database.
148+ data_files_write_file_callback : Callback to write a file to the data files directory and add it to the database.
145149 data_files_filecount: Callback to check the amount of data files already present in the data files directory.
146150 """
147- self ._data_files_write_csv_callback = data_files_write_csv_callback
151+ self ._data_files_write_file_cb = data_files_write_file_cb
148152 self ._data_files_filecount = data_files_filecount
149153
150- async def write_csv (self , write_data : GenericCsvTransform ) -> str :
151- """Writes the provided CSV object to a file in the Data Files directory. Returns the File ID of the file created."""
154+ async def write_file (
155+ self ,
156+ data : bytes ,
157+ mime_type : MimeType ,
158+ command_metadata : CommandFileNameMetadata = None ,
159+ ) -> str :
160+ """Writes arbitrary data to a file in the Data Files directory. Returns the File ID of the file created."""
152161 if self ._data_files_filecount is not None :
153162 file_count = await self ._data_files_filecount ()
154- if file_count >= MAXIMUM_CSV_FILE_LIMIT :
163+ if file_count >= MAXIMUM_FILE_LIMIT :
155164 raise StorageLimitReachedError (
156- f"Not enough space to store file { write_data .filename } ."
165+ f"Not enough space to store file. Maximum file limit of { MAXIMUM_FILE_LIMIT } reached."
166+ )
167+ if self ._data_files_write_file_cb is not None :
168+ file_data = FileData .build (
169+ data = data ,
170+ mime_type = mime_type ,
171+ command_metadata = command_metadata ,
157172 )
158- if self ._data_files_write_csv_callback is not None :
159- return await self ._data_files_write_csv_callback (write_data )
173+ return await self ._data_files_write_file_cb (file_data )
160174 # If we are in an analysis or simulation state, return an empty file ID
161175 return ""
0 commit comments