wingechr-datatools 0.14.0__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.
- datatools/__init__.py +15 -0
- datatools/base.py +72 -0
- datatools/converter.py +268 -0
- datatools/process.py +382 -0
- datatools/storage.py +438 -0
- datatools/utils.py +893 -0
- wingechr_datatools-0.14.0.dist-info/METADATA +25 -0
- wingechr_datatools-0.14.0.dist-info/RECORD +10 -0
- wingechr_datatools-0.14.0.dist-info/WHEEL +5 -0
- wingechr_datatools-0.14.0.dist-info/top_level.txt +1 -0
datatools/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
__version__ = "0.14.0"
|
|
2
|
+
|
|
3
|
+
from datatools import utils
|
|
4
|
+
from datatools.converter import Converter
|
|
5
|
+
from datatools.process import Function
|
|
6
|
+
from datatools.storage import Metadata, Resource, Storage
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Storage",
|
|
10
|
+
"Resource",
|
|
11
|
+
"Metadata",
|
|
12
|
+
"Function",
|
|
13
|
+
"Converter",
|
|
14
|
+
"utils",
|
|
15
|
+
]
|
datatools/base.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import DEFAULT_BUFFER_SIZE as _DEFAULT_BUFFER_SIZE
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Callable, Dict, Union, cast
|
|
5
|
+
|
|
6
|
+
import appdirs
|
|
7
|
+
|
|
8
|
+
Type = Union[type, str, None, type(Callable)]
|
|
9
|
+
ResourceName = str
|
|
10
|
+
MetadataKey = str
|
|
11
|
+
MetadataValue = Any
|
|
12
|
+
ParameterKey = Union[None, int, str]
|
|
13
|
+
ParamterTypes = dict[str, Type]
|
|
14
|
+
OptionalStr = Union[str, None]
|
|
15
|
+
StrPath = Union[Path, str]
|
|
16
|
+
UriHandlerType = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
FUNCTION_URI_PREFIX = "function://"
|
|
20
|
+
PROCESS_URI_PREFIX = "process://"
|
|
21
|
+
PARAM_SQL_QUERY = "q"
|
|
22
|
+
ROOT_METADATA_PATH = "$" # root
|
|
23
|
+
MEDIA_TYPE_METADATA_PATH = "mediaType"
|
|
24
|
+
HASHED_DATA_PATH_PREFIX = "hash"
|
|
25
|
+
HASH_METHODS = ["md5", "sha256"]
|
|
26
|
+
DEFAULT_HASH_METHOD: str = HASH_METHODS[0]
|
|
27
|
+
|
|
28
|
+
user_data_dir = cast(
|
|
29
|
+
str,
|
|
30
|
+
appdirs.user_data_dir( # type:ignore
|
|
31
|
+
appname="datatools", appauthor=None, version=None, roaming=False
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
DEFAULT_LOCAL_LOCATION = "__data__"
|
|
35
|
+
GLOBAL_LOCATION = os.path.join(
|
|
36
|
+
user_data_dir,
|
|
37
|
+
DEFAULT_LOCAL_LOCATION,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
DEFAULT_BUFFER_SIZE = _DEFAULT_BUFFER_SIZE # default is 1024 * 8, wsgi uses 1024 * 16
|
|
41
|
+
DATETIMETZ_FMT = "%Y-%m-%dT%H:%M:%S%z"
|
|
42
|
+
DATE_FMT = "%Y-%m-%d"
|
|
43
|
+
TIME_FMT = "%H:%M:%S"
|
|
44
|
+
FILEMOD_WRITE = 0o222
|
|
45
|
+
ANONYMOUS_USER = "Anonymous"
|
|
46
|
+
LOCALHOST = "localhost"
|
|
47
|
+
STORAGE_SCHEME = "data"
|
|
48
|
+
RESOURCE_URI_PREFIX = f"{STORAGE_SCHEME}:///"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MetadataDict(Dict[str, Any]):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DatatoolsException(Exception):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StorageException(DatatoolsException):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class InvalidPathException(StorageException):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ConverterException(DatatoolsException):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ProcessException(DatatoolsException):
|
|
72
|
+
pass
|
datatools/converter.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Type conversion"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import pickle
|
|
6
|
+
from io import BufferedIOBase, BytesIO, TextIOWrapper
|
|
7
|
+
from itertools import product
|
|
8
|
+
from typing import Any, Callable, ClassVar, Optional, Union
|
|
9
|
+
from urllib.parse import parse_qs, urlsplit
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from datatools.base import (
|
|
15
|
+
PARAM_SQL_QUERY,
|
|
16
|
+
MetadataDict,
|
|
17
|
+
OptionalStr,
|
|
18
|
+
ParameterKey,
|
|
19
|
+
Type,
|
|
20
|
+
UriHandlerType,
|
|
21
|
+
)
|
|
22
|
+
from datatools.utils import (
|
|
23
|
+
copy_signature,
|
|
24
|
+
filepath_from_uri,
|
|
25
|
+
get_function_datatype,
|
|
26
|
+
get_function_name,
|
|
27
|
+
get_function_parameters_datatypes,
|
|
28
|
+
get_type_name,
|
|
29
|
+
json_serialize,
|
|
30
|
+
passthrough,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = ["Converter"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def clean_type(dtype: Type) -> OptionalStr:
|
|
37
|
+
if isinstance(dtype, type):
|
|
38
|
+
return get_type_name(dtype)
|
|
39
|
+
return dtype
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_cleaned_type_list(types: Union[Type, list[Type]]) -> list[OptionalStr]:
|
|
43
|
+
if not isinstance(types, list):
|
|
44
|
+
types = [types]
|
|
45
|
+
return [clean_type(x) for x in types]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Converter:
|
|
49
|
+
_converters: ClassVar[
|
|
50
|
+
dict[tuple[Union[str, None], Union[str, None]], Callable[..., Any]]
|
|
51
|
+
] = {}
|
|
52
|
+
|
|
53
|
+
def __init__(self, function: Callable[..., Any]):
|
|
54
|
+
self.function = function
|
|
55
|
+
copy_signature(self, self.function)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get(cls, type_from: Type, type_to: Type) -> Callable[..., Any]:
|
|
59
|
+
type_from = clean_type(type_from)
|
|
60
|
+
type_to = clean_type(type_to)
|
|
61
|
+
if type_from == type_to:
|
|
62
|
+
return passthrough
|
|
63
|
+
return cls._converters[(type_from, type_to)]
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def register(
|
|
67
|
+
cls,
|
|
68
|
+
type_from: Union[Type, list[Type]],
|
|
69
|
+
type_to: Union[Type, list[Type]],
|
|
70
|
+
) -> Callable[..., Any]:
|
|
71
|
+
types_from = get_cleaned_type_list(type_from)
|
|
72
|
+
types_to = get_cleaned_type_list(type_to)
|
|
73
|
+
|
|
74
|
+
def decorator(function: Callable[..., Any]) -> Converter:
|
|
75
|
+
converter = Converter(function=function)
|
|
76
|
+
for tf_tt in product(types_from, types_to):
|
|
77
|
+
if tf_tt in cls._converters:
|
|
78
|
+
logging.warning(
|
|
79
|
+
"Overwriting exising Converter %s, %s (%s, %s)",
|
|
80
|
+
*tf_tt,
|
|
81
|
+
get_function_name(cls._converters[tf_tt]),
|
|
82
|
+
get_function_name(converter),
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
logging.debug("Registering Converter %s, %s", *tf_tt)
|
|
86
|
+
cls._converters[tf_tt] = converter
|
|
87
|
+
return converter
|
|
88
|
+
|
|
89
|
+
return decorator
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def register_uri_handler(
|
|
93
|
+
cls,
|
|
94
|
+
scheme_from: Union[Type, list[Type]],
|
|
95
|
+
) -> Callable[..., Any]:
|
|
96
|
+
return cls.register(scheme_from, UriHandlerType)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def autoregister(
|
|
100
|
+
cls,
|
|
101
|
+
function: Callable[..., Any],
|
|
102
|
+
) -> Callable[..., Any]:
|
|
103
|
+
type_from = list(get_function_parameters_datatypes(function).values())[0]
|
|
104
|
+
type_to = get_function_datatype(function)
|
|
105
|
+
return cls.register(type_from=type_from, type_to=type_to)(function)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def convert_return(
|
|
109
|
+
cls, type_to: Type, type_from: Type = None
|
|
110
|
+
) -> Callable[..., Any]:
|
|
111
|
+
if type_from is None:
|
|
112
|
+
# get converter after function returned result
|
|
113
|
+
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
114
|
+
def decorated_function(*args: Any, **kwargs: Any):
|
|
115
|
+
result = function(*args, **kwargs)
|
|
116
|
+
type_from = get_type_name(type(result))
|
|
117
|
+
converter = Converter.get(type_from, type_to)
|
|
118
|
+
return converter(result)
|
|
119
|
+
|
|
120
|
+
return decorated_function
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
# get converter bofore function returned result
|
|
124
|
+
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
125
|
+
converter = Converter.get(type_from, type_to)
|
|
126
|
+
|
|
127
|
+
def decorated_function(*args: Any, **kwargs: Any):
|
|
128
|
+
result = function(*args, **kwargs)
|
|
129
|
+
return converter(result)
|
|
130
|
+
|
|
131
|
+
return decorated_function
|
|
132
|
+
|
|
133
|
+
return decorator
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def convert_to(cls, data: Type, type_to: Type = None, **kwargs: Any) -> Any:
|
|
137
|
+
type_from = get_type_name(type(data))
|
|
138
|
+
convert = Converter.get(type_from, type_to)
|
|
139
|
+
return convert(data, **kwargs)
|
|
140
|
+
|
|
141
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
142
|
+
return self.function(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
# def __get__(self, instance: Any, owner: Any):
|
|
145
|
+
# # Support instance methods
|
|
146
|
+
# return self.__class__(self.function.__get__(instance, owner))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# register some default converters
|
|
150
|
+
|
|
151
|
+
json_types: list[Type] = [get_type_name(x) for x in [list, dict]] # type:ignore
|
|
152
|
+
pickle_types: list[Type] = [
|
|
153
|
+
get_type_name(x) for x in [list, dict, pd.DataFrame] # type:ignore
|
|
154
|
+
]
|
|
155
|
+
sql_protocols: list[Type] = ["sqlite:"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@Converter.register(json_types, ".json")
|
|
159
|
+
def json_dump(data: object, encoding: str = "utf-8") -> BufferedIOBase:
|
|
160
|
+
return BytesIO(
|
|
161
|
+
json.dumps(
|
|
162
|
+
data, indent=2, ensure_ascii=False, sort_keys=False, default=json_serialize
|
|
163
|
+
).encode(encoding=encoding)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@Converter.register(".json", json_types)
|
|
168
|
+
def json_load(buffer: BytesIO, encoding: str = "utf-8") -> object:
|
|
169
|
+
with TextIOWrapper(buffer, encoding=encoding) as text_buffer:
|
|
170
|
+
return json.load(text_buffer)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@Converter.register(pickle_types, ".pickle")
|
|
174
|
+
def pickle_dump(data: object) -> BufferedIOBase:
|
|
175
|
+
return BytesIO(pickle.dumps(data))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@Converter.register(".pickle", pickle_types)
|
|
179
|
+
def pickle_load(buffer: BufferedIOBase) -> object:
|
|
180
|
+
return pickle.load(buffer)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@Converter.register_uri_handler(["https:", "http:"])
|
|
184
|
+
def download(url: str, headers: Union[dict[Any, Any], None] = None) -> BufferedIOBase:
|
|
185
|
+
"""Download content from a URL and return it as a BufferedIOBase object."""
|
|
186
|
+
response: requests.Response = requests.get(url, headers=headers) # type:ignore
|
|
187
|
+
response.raise_for_status() # Raise an error for bad responses # type:ignore
|
|
188
|
+
return BytesIO(response.content) # type:ignore
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@Converter.register_uri_handler("file:")
|
|
192
|
+
def filecopy(url: str) -> BufferedIOBase:
|
|
193
|
+
"""Copy file."""
|
|
194
|
+
path = filepath_from_uri(url)
|
|
195
|
+
return path.open("rb")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@Converter.register(pd.DataFrame, ".json")
|
|
199
|
+
def dataframe_to_json(df: pd.DataFrame) -> BufferedIOBase:
|
|
200
|
+
# buf = BytesIO()
|
|
201
|
+
data = df.to_dict(orient="records") # type: ignore
|
|
202
|
+
return json_dump(data)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@Converter.register(".json", pd.DataFrame)
|
|
206
|
+
def json_to_dataframe(buffer: BufferedIOBase, encoding: str = "utf-8") -> pd.DataFrame:
|
|
207
|
+
with TextIOWrapper(buffer, encoding=encoding) as text_buffer: # type: ignore
|
|
208
|
+
return pd.read_json(text_buffer) # type:ignore
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@Converter.register(".csv", pd.DataFrame)
|
|
212
|
+
def csv_to_dataframe(
|
|
213
|
+
buffer: BufferedIOBase,
|
|
214
|
+
encoding: str = "utf-8",
|
|
215
|
+
index_col: Optional[list[ParameterKey]] = None,
|
|
216
|
+
) -> pd.DataFrame:
|
|
217
|
+
with TextIOWrapper(buffer, encoding=encoding) as text_buffer: # type: ignore
|
|
218
|
+
df = pd.read_csv(text_buffer, index_col=index_col) # type:ignore
|
|
219
|
+
return df
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@Converter.register(".xlsx", pd.DataFrame)
|
|
223
|
+
def xlsx_to_dataframe(buffer: BufferedIOBase) -> pd.DataFrame:
|
|
224
|
+
with buffer:
|
|
225
|
+
return pd.read_excel(buffer) # type:ignore
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@Converter.register_uri_handler(sql_protocols)
|
|
229
|
+
def sql_download(uri: str) -> pd.DataFrame:
|
|
230
|
+
"""Copy data from sql database"""
|
|
231
|
+
query = parse_qs(urlsplit(uri).query)
|
|
232
|
+
sql_query = query.get(PARAM_SQL_QUERY)
|
|
233
|
+
if not sql_query:
|
|
234
|
+
raise ValueError("Missing sql query")
|
|
235
|
+
sql_query = sql_query[0]
|
|
236
|
+
df = pd.read_sql(sql_query, uri) # type:ignore
|
|
237
|
+
return df
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@Converter.autoregister
|
|
241
|
+
def get_handler(url: str) -> Callable[..., Any]:
|
|
242
|
+
scheme = url.split(":")[0]
|
|
243
|
+
return Converter.get(f"{scheme}:", None)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@Converter.register(pd.DataFrame, MetadataDict)
|
|
247
|
+
def inspect_df(df: pd.DataFrame) -> dict[str, Any]:
|
|
248
|
+
index_col: list[ParameterKey] = [
|
|
249
|
+
c if c is not None else 0 for c in df.index.names # type:ignore
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"columns": df.columns.tolist(),
|
|
254
|
+
# "dtypes": df.dtypes.to_dict(),
|
|
255
|
+
"shape": df.shape,
|
|
256
|
+
# index_col: important for loader
|
|
257
|
+
"index_col": index_col,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@Converter.register(pd.DataFrame, ".csv")
|
|
262
|
+
def df_to_csv(df: pd.DataFrame, encoding: str = "utf-8", index: bool = True):
|
|
263
|
+
buf = BytesIO()
|
|
264
|
+
# TODO: if index = True but has no names: generate names,
|
|
265
|
+
# otherwise they will be renamed when loading (e.g. Unnamed: 0)
|
|
266
|
+
df.to_csv(buf, encoding=encoding, index=index)
|
|
267
|
+
buf.seek(0)
|
|
268
|
+
return buf
|