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 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