Skip to content

Commit 56acba2

Browse files
authored
Merge pull request #109 from Point72/ac/filedrop
Add custom loader/deserializer, drop field_map from filedrop module
2 parents 6bf61c0 + dd552de commit 56acba2

3 files changed

Lines changed: 195 additions & 105 deletions

File tree

csp_gateway/server/modules/filedrop/adapter.py

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from dataclasses import dataclass
44
from datetime import datetime
55
from enum import Enum, auto
6-
from typing import Any, Dict, List, Optional, TypeVar, get_args, get_origin
6+
from typing import Any, Callable, Dict, List, Optional, TypeVar
77

8+
import csp
89
import orjson
910
import pyarrow.parquet as pq
10-
from csp import ts
1111
from csp.impl.pushadapter import PushInputAdapter
1212
from csp.impl.types.container_type_normalizer import ContainerTypeNormalizer
1313
from csp.impl.wiring import py_push_adapter_def
@@ -26,6 +26,7 @@
2626

2727

2828
class FileDropType(Enum):
29+
CUSTOM = auto()
2930
CSV = auto()
3031
JSON = auto()
3132
PARQUET = auto()
@@ -39,46 +40,35 @@ class FileDropAdapterConfiguration:
3940
dir_path: str
4041
# Format of files to expect to load properly i.e parquet, json, csv
4142
filedrop_type: FileDropType
42-
# Map the data fields from the file to the fields of the structs
43-
field_map: Dict[str, str]
4443
# List of extensions to filter, empty list means all extensions are allowed
4544
extensions: List[str]
45+
# custom loader for loading files into list of struct like data
46+
loader: Optional[Callable[[str], List[Any]]]
47+
# deserialize each data point to data type expected by channel type
48+
deserializer: Optional[Callable[[Any], Any]]
4649
# Extra args to the type adapter deserializer
4750
type_adapter_args: Dict[str, Any]
4851

4952

5053
class FileReaderBase:
5154
"""The base file reader that reads data from files and generates structs"""
5255

53-
def __init__(self, config: FileDropAdapterConfiguration, ts_typ: object, deserializer: Optional[object] = None):
54-
self.field_map = config.field_map
56+
def __init__(self, config: FileDropAdapterConfiguration, ts_typ: object):
5557
self.extensions = config.extensions
5658
if hasattr(config, "type_adapter_args"):
5759
self.context = config.type_adapter_args
5860
else:
5961
self.context = {}
60-
if not deserializer:
62+
if hasattr(config, "deserializer") and config.deserializer:
63+
self.deserializer = config.deserializer
64+
else:
6165
normalized_type = ContainerTypeNormalizer.normalize_type(ts_typ)
6266
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:
7567

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)
68+
def deserialize_tick(data, type_adapter=type_adapter, context=self.context):
69+
return type_adapter.validate_python(data, context=context)
7870

7971
self.deserializer = deserialize_tick
80-
else:
81-
self.deserializer = deserializer
8272

8373
def read(self, src_path: str) -> object:
8474
"""Generator to return stucts from a filepath"""
@@ -98,23 +88,11 @@ def read_impl(self, src_path: str) -> List[dict]:
9888

9989
raise Exception(f"read not implemented for {self}")
10090

101-
def apply_field_map(self, data: dict) -> dict:
102-
"""Convert the keys in the data to the field names of the struct"""
103-
104-
if self.field_map:
105-
new_data = {}
106-
for k, v in data.items():
107-
new_k = self.field_map.get(k, k)
108-
new_data[new_k] = v
109-
return new_data
110-
else:
111-
return data
112-
11391

11492
class FileReaderCsv(FileReaderBase):
11593
"""File reader for json file type"""
11694

117-
def read_impl(self, src_path: str) -> List[dict]:
95+
def read_impl(self, src_path: str) -> List[Any]:
11896
data = []
11997
with open(src_path, "r") as f:
12098
reader = csv.DictReader(f)
@@ -126,7 +104,7 @@ def read_impl(self, src_path: str) -> List[dict]:
126104
class FileReaderJson(FileReaderBase):
127105
"""File reader for json file type"""
128106

129-
def read_impl(self, src_path: str) -> List[dict]:
107+
def read_impl(self, src_path: str) -> List[Any]:
130108
with open(src_path, "rb") as f:
131109
data = orjson.loads(f.read())
132110
if isinstance(data, list):
@@ -139,11 +117,22 @@ def read_impl(self, src_path: str) -> List[dict]:
139117
class FileReaderParquet(FileReaderBase):
140118
"""File reader for parquet file type"""
141119

142-
def read_impl(self, src_path: str) -> List[dict]:
120+
def read_impl(self, src_path: str) -> List[Any]:
143121
table = pq.read_table(src_path)
144122
return table.to_pylist()
145123

146124

125+
class FileReaderCustom(FileReaderBase):
126+
"""File reader for a custom file loader"""
127+
128+
def __init__(self, config: FileDropAdapterConfiguration, ts_typ: object):
129+
super().__init__(config, ts_typ)
130+
self._loader = config.loader
131+
132+
def read_impl(self, src_path: str) -> List[Any]:
133+
return self._loader(src_path)
134+
135+
147136
class EventHandlerCustom(FileSystemEventHandler):
148137
def __init__(self, adapter: PushInputAdapter, file_reader: FileReaderBase):
149138
self.file_reader = file_reader
@@ -185,14 +174,15 @@ class _FileDropImpl(PushInputAdapter):
185174
FileDropType.CSV: FileReaderCsv,
186175
FileDropType.JSON: FileReaderJson,
187176
FileDropType.PARQUET: FileReaderParquet,
177+
FileDropType.CUSTOM: FileReaderCustom,
188178
}
189179

190-
def __init__(self, config: FileDropAdapterConfiguration, ts_typ: T, deserializer: Optional[object] = None):
180+
def __init__(self, config: FileDropAdapterConfiguration, ts_typ: T):
191181
# NOTE ts_typ is assumed to be a List["Y"] type where "Y" is the actual type
192182
self.dir_path = config.dir_path
193183
self.observer = Observer()
194184
reader = self.FILEREADER_MAP[config.filedrop_type]
195-
file_reader = reader(config, ts_typ[0], deserializer)
185+
file_reader = reader(config, ts_typ[0])
196186
self.event_handler = EventHandlerCustom(self, file_reader)
197187
self.observer.schedule(self.event_handler, self.dir_path, recursive=False)
198188

@@ -213,11 +203,17 @@ def stop(self):
213203
# Further we can only take the object type as output due to issues caused by using List[T] instead of T and bypassing
214204
# the type normalization process in csp. The user of the adapter, needs to keep track of the type they pass in ts_type,
215205
# and cast the ts[object] to the required ts[T], they want using csp.appy
216-
filedrop_adapter_def = py_push_adapter_def(
217-
"filedrop_adapter_def",
206+
_filedrop_adapter_def = py_push_adapter_def(
207+
"_filedrop_adapter_def",
218208
_FileDropImpl,
219-
ts[object],
209+
csp.ts[object],
220210
config=FileDropAdapterConfiguration,
221211
ts_typ=List[T],
222-
deserializer=Optional[object],
223212
)
213+
214+
215+
@csp.graph
216+
def filedrop_adapter_def(config: FileDropAdapterConfiguration, ts_typ: "T") -> csp.ts["T"]:
217+
object_data = _filedrop_adapter_def(config=config, ts_typ=[ts_typ])
218+
data = csp.apply(object_data, lambda x: x, ts_typ)
219+
return data

csp_gateway/server/modules/filedrop/filedrop.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
22
import os
3-
from typing import Dict, List, TypeVar, get_args, get_origin
3+
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, get_args, get_origin
44

55
import csp
66
from ccflow import BaseModel
77
from csp.impl.types.container_type_normalizer import ContainerTypeNormalizer
88
from csp.impl.types.tstype import isTsType
9-
from pydantic import Field
9+
from pydantic import Field, model_validator
1010

1111
from csp_gateway.server import (
1212
GatewayChannels,
@@ -27,11 +27,12 @@ class ReadFileDropConfiguration(BaseModel):
2727
"""The configuration of a filedrop adapter for a directory and filetype"""
2828

2929
channel_name: str = Field(description="Name of the channel to send the structs to")
30-
filedrop_type: FileDropType = Field(description="The type of files to expect and accordingly read i.e. parquet, json, etc")
31-
field_map: Dict[str, str] = Field(
32-
default={}, description="A map to convert the keys in the data to the field names in the struct type of the channel"
33-
)
30+
filedrop_type: Union[FileDropType, str] = Field(description="The type of files to expect and accordingly read i.e. parquet, json, etc")
3431
extensions: List[str] = Field(default=[], description="List of extensions to decide which files to read, empty list means all extensions")
32+
loader: Optional[Callable[[str], List[Any]]] = Field(default=None, description="custom loader for loading files into list of struct like data")
33+
deserializer: Optional[Callable[[Any], Any]] = Field(
34+
default=None, description="deserialize each data point to data type expected by channel type"
35+
)
3536
subscribe_with_struct_id: bool = Field(
3637
default=False,
3738
description=("If False, replaces the id field on GatewayStructs from files with one autogenerated by the current Gateway."),
@@ -41,6 +42,17 @@ class ReadFileDropConfiguration(BaseModel):
4142
description=("If False, replaces the timestamp field on the GatewayStruct with a timestamp autogenerated by the current Gateway."),
4243
)
4344

45+
@model_validator(mode="after")
46+
def check_filedrop_type(self) -> "ReadFileDropConfiguration":
47+
if isinstance(self.filedrop_type, str):
48+
try:
49+
self.filedrop_type = FileDropType[self.filedrop_type]
50+
except KeyError:
51+
raise ValueError(f"{self.filedrop_type} is not a valid FileDropType")
52+
if self.filedrop_type == FileDropType.CUSTOM and self.loader is None:
53+
raise ValueError("loader must be set if filedrop_type is CUSTOM")
54+
return self
55+
4456

4557
T = TypeVar("T")
4658
K = TypeVar("K")
@@ -81,9 +93,10 @@ def connect(self, channels: GatewayChannels):
8193
adapter_config = FileDropAdapterConfiguration(
8294
dir_path=dir,
8395
filedrop_type=config.filedrop_type,
84-
field_map=config.field_map,
8596
extensions=config.extensions,
8697
type_adapter_args=context,
98+
loader=config.loader,
99+
deserializer=config.deserializer,
87100
)
88101
channel_type = channels.get_outer_type(config.channel_name)
89102
if isTsType(channel_type):
@@ -108,15 +121,13 @@ def connect(self, channels: GatewayChannels):
108121
else:
109122
raise Exception(f"Channel type cannot be handled: {channel_type}")
110123
channel_base_types[config.channel_name] = non_ts_type
111-
# NOTE: We have to pass in the type as [type] because of the type normalization issue in csp: https://github.com/Point72/csp/issues/569
112-
data = filedrop_adapter_def(config=adapter_config, ts_typ=[non_ts_type], deserializer=None)
124+
data = filedrop_adapter_def(config=adapter_config, ts_typ=non_ts_type)
113125
channel_data[config.channel_name].append(data)
114126
for channel_name, data_list in channel_data.items():
115127
data = csp.flatten(data_list)
116-
typed_data = csp.apply(data, lambda x: x, channel_base_types[channel_name])
117128
if channel_basket_types[channel_name] == "list":
118-
channels.set_channel(channel_name, self.handle_list_basket(typed_data, list_size=channels.dynamic_keys()[channel_name]))
129+
channels.set_channel(channel_name, self.handle_list_basket(data, list_size=channels.dynamic_keys()[channel_name]))
119130
elif channel_basket_types[channel_name] == "dict":
120-
channels.set_channel(channel_name, self.handle_dict_basket(typed_data, dict_keys=channels.dynamic_keys()[channel_name]))
131+
channels.set_channel(channel_name, self.handle_dict_basket(data, dict_keys=channels.dynamic_keys()[channel_name]))
121132
else:
122-
channels.set_channel(channel_name, typed_data)
133+
channels.set_channel(channel_name, data)

0 commit comments

Comments
 (0)