33from dataclasses import dataclass
44from datetime import datetime
55from enum import Enum , auto
6- from typing import Any , Dict , List , get_args , get_origin
6+ from typing import Any , Dict , List , Optional , TypeVar , get_args , get_origin
77
88import orjson
99import pyarrow .parquet as pq
1010from csp import ts
1111from csp .impl .pushadapter import PushInputAdapter
1212from csp .impl .types .container_type_normalizer import ContainerTypeNormalizer
1313from csp .impl .wiring import py_push_adapter_def
14+ from pydantic import TypeAdapter
1415from watchdog .events import FileSystemEvent , FileSystemEventHandler
1516from watchdog .observers import Observer
1617
@@ -49,21 +50,35 @@ class FileDropAdapterConfiguration:
4950class FileReaderBase :
5051 """The base file reader that reads data from files and generates structs"""
5152
52- def __init__ (self , config : FileDropAdapterConfiguration , ts_typ : object ):
53+ def __init__ (self , config : FileDropAdapterConfiguration , ts_typ : object , deserializer : Optional [ object ] = None ):
5354 self .field_map = config .field_map
5455 self .extensions = config .extensions
55- normalized_type = ContainerTypeNormalizer .normalize_type (ts_typ )
56- self .is_list = get_origin (normalized_type ) is list
57- if self .is_list :
58- inner_type = get_args (normalized_type )[0 ]
59- type_adapter = inner_type .type_adapter ()
60- else :
61- type_adapter = ts_typ .type_adapter ()
62- self .type_adapter = type_adapter
6356 if hasattr (config , "type_adapter_args" ):
6457 self .context = config .type_adapter_args
6558 else :
6659 self .context = {}
60+ if not deserializer :
61+ normalized_type = ContainerTypeNormalizer .normalize_type (ts_typ )
62+ type_adapter = TypeAdapter (normalized_type )
63+ if get_origin (normalized_type ) is list :
64+
65+ def deserialize_tick (data , type_adapter = type_adapter , apply_field_map = self .apply_field_map , context = self .context ):
66+ data = [apply_field_map (d ) for d in data ]
67+ return type_adapter .validate_python (data , context = context )
68+ elif get_origin (normalized_type ) is dict :
69+ key_type , inner_type = get_args (normalized_type )
70+
71+ def deserialize_tick (data , type_adapter = type_adapter , apply_field_map = self .apply_field_map , context = self .context ):
72+ data = {k : apply_field_map (d ) for k , d in data .items ()}
73+ return type_adapter .validate_python (data , context = context )
74+ else :
75+
76+ def deserialize_tick (data , type_adapter = type_adapter , apply_field_map = self .apply_field_map , context = self .context ):
77+ return type_adapter .validate_python (apply_field_map (data ), context = context )
78+
79+ self .deserializer = deserialize_tick
80+ else :
81+ self .deserializer = deserializer
6782
6883 def read (self , src_path : str ) -> object :
6984 """Generator to return stucts from a filepath"""
@@ -74,12 +89,9 @@ def read(self, src_path: str) -> object:
7489 should_read = False
7590 if should_read :
7691 dicts = self .read_impl (src_path )
77- structs = [self .deserialize_dict (self .apply_field_map (v )) for v in dicts ]
78- if self .is_list :
79- yield structs
80- else :
81- for s in structs :
82- yield s
92+ structs = [self .deserializer (d ) for d in dicts ]
93+ for s in structs :
94+ yield s
8395
8496 def read_impl (self , src_path : str ) -> List [dict ]:
8597 """File type specific implementation"""
@@ -98,11 +110,6 @@ def apply_field_map(self, data: dict) -> dict:
98110 else :
99111 return data
100112
101- def deserialize_dict (self , dict : dict ) -> object :
102- """Convert a dict to struct"""
103-
104- return self .type_adapter .validate_python (dict , context = self .context )
105-
106113
107114class FileReaderCsv (FileReaderBase ):
108115 """File reader for json file type"""
@@ -170,18 +177,22 @@ def on_closed(self, event: FileSystemEvent):
170177 log .error (f"Failed to read data from { file_path } with exception: { e } , skipping" )
171178
172179
180+ T = TypeVar ("T" )
181+
182+
173183class _FileDropImpl (PushInputAdapter ):
174184 FILEREADER_MAP = {
175185 FileDropType .CSV : FileReaderCsv ,
176186 FileDropType .JSON : FileReaderJson ,
177187 FileDropType .PARQUET : FileReaderParquet ,
178188 }
179189
180- def __init__ (self , config : FileDropAdapterConfiguration , ts_typ : "T" ): # noqa
190+ def __init__ (self , config : FileDropAdapterConfiguration , ts_typ : T , deserializer : Optional [object ] = None ):
191+ # NOTE ts_typ is assumed to be a List["Y"] type where "Y" is the actual type
181192 self .dir_path = config .dir_path
182193 self .observer = Observer ()
183194 reader = self .FILEREADER_MAP [config .filedrop_type ]
184- file_reader = reader (config , ts_typ )
195+ file_reader = reader (config , ts_typ [ 0 ], deserializer )
185196 self .event_handler = EventHandlerCustom (self , file_reader )
186197 self .observer .schedule (self .event_handler , self .dir_path , recursive = False )
187198
@@ -195,10 +206,18 @@ def stop(self):
195206 self .observer .join ()
196207
197208
209+ # NOTE: Ref: https://github.com/Point72/csp/issues/569
210+ # Instead of passing the actual type T we want as ts_typ, we have to pass in List[T]
211+ # This is due to a type normalization bug in csp, where it converts Dict[K, T] to dict, i.e removing the type annotation hints
212+ # The type annotations are needed to properly deserialize the raw data using the pydantic type adapters.
213+ # Further we can only take the object type as output due to issues caused by using List[T] instead of T and bypassing
214+ # the type normalization process in csp. The user of the adapter, needs to keep track of the type they pass in ts_type,
215+ # and cast the ts[object] to the required ts[T], they want using csp.appy
198216filedrop_adapter_def = py_push_adapter_def (
199217 "filedrop_adapter_def" ,
200218 _FileDropImpl ,
201- ts ["T" ],
219+ ts [object ],
202220 config = FileDropAdapterConfiguration ,
203- ts_typ = "T" ,
221+ ts_typ = List [T ],
222+ deserializer = Optional [object ],
204223)
0 commit comments