fastapi-rtk 1.0.3__py3-none-any.whl → 1.0.5__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.
@@ -0,0 +1 @@
1
+ __version__ = "1.0.5"
@@ -2735,11 +2735,8 @@ class ModelRestApi(BaseApi):
2735
2735
 
2736
2736
  async for chunk in data:
2737
2737
  for item in chunk:
2738
- async with AsyncTaskRunner():
2739
- item_model = schema.model_validate(item, from_attributes=True)
2740
- item_dict = item_model.model_dump(mode="json")
2741
- row = CSVJSONConverter._json_to_csv(
2742
- item_dict,
2738
+ row = await CSVJSONConverter.ajson_to_csv_single(
2739
+ item,
2743
2740
  list_columns=list_columns,
2744
2741
  delimiter=delimiter,
2745
2742
  export_mode=export_mode,
@@ -2,6 +2,7 @@ import asyncio
2
2
  import contextvars
3
3
  import functools
4
4
  import inspect
5
+ import traceback as tb
5
6
  import typing
6
7
 
7
8
  from .prettify_dict import prettify_dict
@@ -233,7 +234,9 @@ class AsyncTaskRunner:
233
234
  f'Task {index + 1}': {
234
235
  'message': str(exc),
235
236
  'caller': exc.caller,
236
- 'traceback': exc.original_exception.__traceback__,
237
+ 'traceback': ''.join(
238
+ tb.format_exception(exc.original_exception)
239
+ ),
237
240
  }
238
241
  for index, exc in enumerate(exceptions)
239
242
  },
@@ -1,8 +1,15 @@
1
+ import abc
1
2
  import csv
2
3
  import enum
4
+ import inspect
3
5
  import json
4
6
  import typing
5
7
 
8
+ from .run_utils import safe_call
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from ..bases import BasicModel
12
+
6
13
  __all__ = ["Line", "CSVJSONConverter"]
7
14
 
8
15
 
@@ -57,7 +64,7 @@ class CSVJSONConverter:
57
64
  @classmethod
58
65
  def json_to_csv(
59
66
  cls,
60
- data: dict[str, typing.Any] | list[dict[str, typing.Any]],
67
+ data: "dict[str, typing.Any] | list[dict[str, typing.Any]] | BasicModel | list[BasicModel]",
61
68
  /,
62
69
  *,
63
70
  list_columns: list[str],
@@ -71,8 +78,10 @@ class CSVJSONConverter:
71
78
  """
72
79
  Converts JSON data to CSV format.
73
80
 
81
+ - Data can also be a subclass of `BasicModel` or a list of subclasses of `BasicModel`.
82
+
74
83
  Args:
75
- data (dict[str, Any] | list[dict[str, Any]]): The JSON data to be converted.
84
+ data (dict[str, typing.Any] | list[dict[str, typing.Any]] | BasicModel | list[BasicModel]): The JSON data to be converted. Can also be a subclass of `BasicModel` or a list of subclasses of `BasicModel`.
76
85
  list_columns (list[str]): The list of columns to be included in the CSV.
77
86
  label_columns (dict[str, str]): The mapping of column names to labels.
78
87
  with_header (bool, optional): Whether to include the header in the CSV. Defaults to True.
@@ -93,11 +102,11 @@ class CSVJSONConverter:
93
102
  writer.writerow(header)
94
103
  csv_data = line.read()
95
104
 
96
- if isinstance(data, dict):
105
+ if not isinstance(data, list):
97
106
  data = [data]
98
107
 
99
108
  for item in data:
100
- row = cls._json_to_csv(
109
+ row = cls.json_to_csv_single(
101
110
  item,
102
111
  list_columns=list_columns,
103
112
  delimiter=delimiter,
@@ -110,9 +119,9 @@ class CSVJSONConverter:
110
119
  return csv_data.strip()
111
120
 
112
121
  @classmethod
113
- def _json_to_csv(
122
+ def json_to_csv_single(
114
123
  self,
115
- data: dict[str, typing.Any],
124
+ data: "dict[str, typing.Any] | BasicModel",
116
125
  /,
117
126
  *,
118
127
  list_columns: list[str],
@@ -123,8 +132,10 @@ class CSVJSONConverter:
123
132
  """
124
133
  Converts single JSON object to CSV format.
125
134
 
135
+ - Data can also be a subclass of `BasicModel`.
136
+
126
137
  Args:
127
- data (dict[str, Any]): The JSON data to be converted.
138
+ data (dict[str, typing.Any] | BasicModel): The JSON data to be converted. Can also be a subclass of `BasicModel`.
128
139
  list_columns (list[str]): The list of columns to be included in the CSV.
129
140
  delimiter (str, optional): The delimiter to use in the CSV. Defaults to ",".
130
141
  relation_separator (str, optional): The separator to use for nested keys. Defaults to ".".
@@ -134,40 +145,60 @@ class CSVJSONConverter:
134
145
  str: The CSV data as a string.
135
146
  """
136
147
  csv_data: list[str] = []
148
+ data_pipeline = DataPipeline()
149
+ data_pipeline.add_processor(ColumnProcessor(relation_separator))
150
+ data_pipeline.add_processor(ModelProcessor())
151
+ data_pipeline.add_processor(
152
+ ListProcessor(delimiter=delimiter, export_mode=export_mode)
153
+ )
154
+ data_pipeline.add_processor(EnumProcessor())
155
+ data_pipeline.add_processor(FallbackProcessor())
137
156
 
138
157
  for col in list_columns:
139
- sub_col = []
140
- if relation_separator in col:
141
- col, *sub_col = col.split(relation_separator)
142
- curr_val = data.get(col, "")
143
- for sub in sub_col:
144
- if isinstance(curr_val, dict):
145
- curr_val = curr_val.get(sub, "")
146
- else:
147
- curr_val = ""
148
-
149
- if isinstance(curr_val, dict):
150
- curr_val = curr_val.get("name_", curr_val)
151
- elif isinstance(curr_val, list):
152
- curr_val = [
153
- curr_val.get(
154
- "id_" if export_mode == "detailed" else "name_",
155
- json.dumps(curr_val),
156
- )
157
- for curr_val in curr_val
158
- ]
159
- array_separator = "," if delimiter == ";" else ";"
160
- curr_val = array_separator.join(curr_val)
161
- elif isinstance(curr_val, enum.Enum):
162
- curr_val = curr_val.value
163
- if curr_val is not None:
164
- if isinstance(curr_val, dict):
165
- curr_val = json.dumps(curr_val)
166
- else:
167
- curr_val = str(curr_val)
168
- else:
169
- curr_val = ""
170
- csv_data.append(curr_val)
158
+ value = data_pipeline.process(data, col)
159
+ csv_data.append(value)
160
+
161
+ return csv_data
162
+
163
+ @classmethod
164
+ async def ajson_to_csv_single(
165
+ cls,
166
+ data: "dict[str, typing.Any] | BasicModel",
167
+ /,
168
+ *,
169
+ list_columns: list[str],
170
+ delimiter=",",
171
+ relation_separator=".",
172
+ export_mode: ExportMode = "simplified",
173
+ ):
174
+ """
175
+ Asynchronously converts single JSON object to CSV format.
176
+
177
+ - Data can also be a `BasicModel`.
178
+
179
+ Args:
180
+ data (dict[str, typing.Any] | BasicModel): The JSON data to be converted. Can also be a `BasicModel`.
181
+ list_columns (list[str]): The list of columns to be included in the CSV.
182
+ delimiter (str, optional): The delimiter to use in the CSV. Defaults to ",".
183
+ relation_separator (str, optional): The separator to use for nested keys. Defaults to ".".
184
+ export_mode (ExportMode, optional): Export mode (simplified or detailed). Defaults to "simplified".
185
+
186
+ Returns:
187
+ str: The CSV data as a string.
188
+ """
189
+ csv_data: list[str] = []
190
+ data_pipeline = DataPipeline()
191
+ data_pipeline.add_processor(AsyncColumnProcessor(relation_separator))
192
+ data_pipeline.add_processor(ModelProcessor())
193
+ data_pipeline.add_processor(
194
+ ListProcessor(delimiter=delimiter, export_mode=export_mode)
195
+ )
196
+ data_pipeline.add_processor(EnumProcessor())
197
+ data_pipeline.add_processor(FallbackProcessor())
198
+
199
+ for col in list_columns:
200
+ value = await data_pipeline.aprocess(data, col)
201
+ csv_data.append(value)
171
202
 
172
203
  return csv_data
173
204
 
@@ -224,3 +255,175 @@ class CSVJSONConverter:
224
255
  value = value.split(list_delimiter)
225
256
  current[parts[-1]] = [item.strip() for item in value if item.strip()]
226
257
  return result
258
+
259
+
260
+ class DataPipeline:
261
+ def __init__(self):
262
+ self.processors = list[DataProcessor]()
263
+
264
+ def add_processor(self, processor: "DataProcessor"):
265
+ """
266
+ Adds a data processor to the pipeline.
267
+
268
+ Args:
269
+ processor (DataProcessor): The data processor to add.
270
+ """
271
+ self.processors.append(processor)
272
+
273
+ def process(self, data: typing.Any, col: str):
274
+ """
275
+ Processes the data through the pipeline.
276
+
277
+ Args:
278
+ data (typing.Any): The data to process.
279
+ col (str): The column to process.
280
+
281
+ Returns:
282
+ typing.Any: The processed data.
283
+ """
284
+ for processor in self.processors:
285
+ data, col, should_continue = processor.process(data, col)
286
+ if not should_continue:
287
+ break
288
+ return data
289
+
290
+ async def aprocess(self, data: typing.Any, col: str):
291
+ """
292
+ Asynchronously processes the data through the pipeline.
293
+
294
+ Args:
295
+ data (typing.Any): The data to process.
296
+ col (str): The column to process.
297
+
298
+ Returns:
299
+ typing.Any: The processed data.
300
+ """
301
+ for processor in self.processors:
302
+ data, col, should_continue = await safe_call(processor.process(data, col))
303
+ if not should_continue:
304
+ break
305
+ return data
306
+
307
+
308
+ class DataProcessor(abc.ABC):
309
+ @abc.abstractmethod
310
+ def process(self, data: typing.Any, col: str) -> tuple[typing.Any, str, bool]:
311
+ """
312
+ Processes the data for a specific column.
313
+
314
+ Args:
315
+ data (typing.Any): The data to process.
316
+ col (str): The column to process.
317
+
318
+ Returns:
319
+ tuple[typing.Any, str, bool]: The processed data, the column name, and a boolean indicating whether to continue processing.
320
+ """
321
+ raise NotImplementedError()
322
+
323
+
324
+ class ColumnProcessor(DataProcessor):
325
+ def __init__(self, relation_separator: str = "."):
326
+ super().__init__()
327
+ self.relation_separator = relation_separator
328
+
329
+ def process(self, data, col):
330
+ sub_col = []
331
+ if self.relation_separator in col:
332
+ col, *sub_col = col.split(self.relation_separator)
333
+ data = data.get(col, "") if isinstance(data, dict) else getattr(data, col, "")
334
+ for sub in sub_col:
335
+ data = (
336
+ data.get(sub, "") if isinstance(data, dict) else getattr(data, sub, "")
337
+ )
338
+ return data, col, True
339
+
340
+
341
+ class AsyncColumnProcessor(ColumnProcessor):
342
+ async def process(self, data, col):
343
+ data, col, continue_processing = super().process(data, col)
344
+ if inspect.iscoroutine(data):
345
+ data = await data
346
+ return data, col, continue_processing
347
+
348
+
349
+ class ModelProcessor(DataProcessor):
350
+ def __init__(self, attr="name_"):
351
+ super().__init__()
352
+ self.attr = attr
353
+
354
+ def process(self, data, col):
355
+ from ..bases import BasicModel
356
+
357
+ continue_processing = True
358
+
359
+ if isinstance(data, BasicModel):
360
+ data = getattr(data, self.attr, "")
361
+ continue_processing = False
362
+ return data, col, continue_processing
363
+
364
+
365
+ class DictProcessor(ModelProcessor):
366
+ def process(self, data, col):
367
+ continue_processing = True
368
+
369
+ if isinstance(data, dict):
370
+ data = data.get(self.attr, json.dumps(data))
371
+ continue_processing = False
372
+ return data, col, continue_processing
373
+
374
+
375
+ class ListProcessor(DataProcessor):
376
+ def __init__(
377
+ self,
378
+ delimiter=",",
379
+ export_mode: CSVJSONConverter.ExportMode = "simplified",
380
+ attr_detailed="id_",
381
+ attr_simplified="name_",
382
+ ):
383
+ super().__init__()
384
+ self.separator = "," if delimiter == ";" else ";"
385
+ self.export_mode = export_mode
386
+ self.model_processor = ModelProcessor(
387
+ attr_detailed if export_mode == "detailed" else attr_simplified
388
+ )
389
+ self.dict_processor = DictProcessor(
390
+ attr_detailed if export_mode == "detailed" else attr_simplified
391
+ )
392
+
393
+ def process(self, data, col):
394
+ from ..bases import BasicModel
395
+
396
+ continue_processing = True
397
+
398
+ if isinstance(data, list):
399
+ processed_list = []
400
+ for item in data:
401
+ if isinstance(item, dict):
402
+ item_processed, _, _ = self.dict_processor.process(item, col)
403
+ elif isinstance(item, BasicModel):
404
+ item_processed, _, _ = self.model_processor.process(item, col)
405
+ else:
406
+ item_processed = str(item)
407
+ processed_list.append(item_processed)
408
+ data = self.separator.join(processed_list)
409
+ continue_processing = False
410
+ return data, col, continue_processing
411
+
412
+
413
+ class EnumProcessor(DataProcessor):
414
+ def process(self, data, col):
415
+ continue_processing = True
416
+
417
+ if isinstance(data, enum.Enum):
418
+ data = data.value
419
+ continue_processing = False
420
+ return data, col, continue_processing
421
+
422
+
423
+ class FallbackProcessor(DataProcessor):
424
+ def process(self, data, col):
425
+ if data is None:
426
+ data = ""
427
+ else:
428
+ data = str(data)
429
+ return data, col, False
fastapi_rtk/version.py CHANGED
@@ -1 +1,6 @@
1
1
  __version__ = "0.0.0"
2
+ # Try to import the version from the generated _version.py file
3
+ try:
4
+ from ._version import __version__ # noqa: F401
5
+ except (ImportError, ModuleNotFoundError):
6
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rtk
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: A package that provides a set of tools to build a FastAPI application with a Class-Based CRUD API.
5
5
  Project-URL: Homepage, https://codeberg.org/datatactics/fastapi-rtk
6
6
  Project-URL: Issues, https://codeberg.org/datatactics/fastapi-rtk/issues
@@ -1,4 +1,5 @@
1
1
  fastapi_rtk/__init__.py,sha256=acLIihNMCZI3prFTq0cru1-k3kPjSZb2hhcqNrW7xJo,6203
2
+ fastapi_rtk/_version.py,sha256=B9kKWJLln1i8LjtkcYecvNWGLTrez4gCUOHtnPlInFo,22
2
3
  fastapi_rtk/apis.py,sha256=6X_Lhl98m7lKrDRybg2Oe24pLFLJ29eCOQSwCAvpKhY,172
3
4
  fastapi_rtk/config.py,sha256=9PZF9E5i1gxmnsZEprZZKxVHSk0dFEklJSplX9NEqdo,14036
4
5
  fastapi_rtk/const.py,sha256=huvh4Zor77fgUkhU4-x6LgkOglSxeKXOlXdhnai_5CQ,4905
@@ -17,10 +18,10 @@ fastapi_rtk/routers.py,sha256=5gMGyH4Any9TwB3Fz23QDkmrX8MouzCxVgoAIcBqnTI,18988
17
18
  fastapi_rtk/schemas.py,sha256=zzzmf5qjSn6PtzrekD3Kll_WOn6XWR7h4AHQchW3GX8,17723
18
19
  fastapi_rtk/setting.py,sha256=vpjnio0mp_yZhDajZaBl6uOFmrBYl-sqB0L_lSkBpfk,19529
19
20
  fastapi_rtk/types.py,sha256=-LPnTIbHvqJW81__gab3EWrhjNmznHhptz0BtXkEAHQ,3612
20
- fastapi_rtk/version.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
21
+ fastapi_rtk/version.py,sha256=D2cmQf2LNeHOiEfcNzVOOfcAmuLvPEmGEtZv5G54D0c,195
21
22
  fastapi_rtk/api/__init__.py,sha256=MwFR7HHppnhbjZGg3sOdQ6nqy9uxnHHXvicpswNFMNA,245
22
23
  fastapi_rtk/api/base_api.py,sha256=42I9v3b25lqxNAMDGEtajA5-btIDSyUWF0xMDgGkA8c,8078
23
- fastapi_rtk/api/model_rest_api.py,sha256=Hp6ws1CjLG-2e-yHxniIudUUmnKff9js2VVZ4Q5JMoo,105111
24
+ fastapi_rtk/api/model_rest_api.py,sha256=PQwR_ya4rrzlZlHDl8lr3z7y1SH8mRbgiVRX2TrjsGk,104924
24
25
  fastapi_rtk/auth/__init__.py,sha256=iX7O41NivBYDfdomEaqm4lUx9KD17wI4g3EFLF6kUTw,336
25
26
  fastapi_rtk/auth/auth.py,sha256=MZmuueioiMbSHjd_F3frKEqCA3yjtanRWyKOy6CnOd0,20994
26
27
  fastapi_rtk/auth/hashers/__init__.py,sha256=uBThFj2VPPSMSioxYTktNiM4-mVgtDAjTpKA3ZzWxxs,110
@@ -105,9 +106,9 @@ fastapi_rtk/security/sqla/apis.py,sha256=J3BCqSnaX-w9raTA5LHa7muW24kA09nAm9kN6b3
105
106
  fastapi_rtk/security/sqla/models.py,sha256=dwIZHNU-6b1r0M2swlYyRtSgI1e1hnvav6d6jdmffM8,8096
106
107
  fastapi_rtk/security/sqla/security_manager.py,sha256=eBXhDTjqQYySCKQXnD6wEeHBGw8oytwVvmKpi9btNYI,17371
107
108
  fastapi_rtk/utils/__init__.py,sha256=0X4BwrVDl4S3mB7DLyHaZVednefMjRIjBIDT3yv_CHM,1875
108
- fastapi_rtk/utils/async_task_runner.py,sha256=PNo0vdKQAUOZ3HqGzU7E232qV8V218hKB7q8_uwx2dM,12653
109
+ fastapi_rtk/utils/async_task_runner.py,sha256=JYXjVPxkWH1pxXImkjlebAOcn2noEUbX-vwpBWw3WGg,12778
109
110
  fastapi_rtk/utils/class_factory.py,sha256=jlVw8yCh-tYsMnR5Hm8fgxtL0kvXwnhe6DPJA1sUh7k,598
110
- fastapi_rtk/utils/csv_json_converter.py,sha256=S0j73joqFw4a2xqhqR53vOMCO2s6Q-eiGubTBysy0kU,7459
111
+ fastapi_rtk/utils/csv_json_converter.py,sha256=7szrPiB7DrK5S-s2GaHVCmEP9_OXk9RFwbZmRtAKM5A,14036
111
112
  fastapi_rtk/utils/deep_merge.py,sha256=PHtKJgXfCngOBGVliX9aVlEFcwCxr-GlzU-w6vjgAIs,2426
112
113
  fastapi_rtk/utils/extender_mixin.py,sha256=eu22VAZJIf-r_uD-zVn_2IzvknfuUkmEHn9oo-0KU0k,1388
113
114
  fastapi_rtk/utils/flask_appbuilder_utils.py,sha256=nPLQIczDZgKElMtqBRSo_aPJZMOnPs7fRyjqKUtPDbo,2276
@@ -125,8 +126,8 @@ fastapi_rtk/utils/timezone.py,sha256=62S0pPWuDFFXxV1YTFCsc4uKiSP_Ba36Fv7S3gYjfhs
125
126
  fastapi_rtk/utils/update_signature.py,sha256=PIzZgNpGEwvDNgQ3G51Zi-QhVV3mbxvUvSwDwf_-yYs,2209
126
127
  fastapi_rtk/utils/use_default_when_none.py,sha256=H2HqhKy_8eYk3i1xijEjuaKak0oWkMIkrdz6T7DK9QU,469
127
128
  fastapi_rtk/utils/werkzeug.py,sha256=1Gv-oyqSmhVGrmNbB9fDqpqJzPpANOzWf4zMMrhW9UA,3245
128
- fastapi_rtk-1.0.3.dist-info/METADATA,sha256=83tmy_FUJBHWFpAIMx1Kd9-h4LJvMou7R-CnVefLSMs,1270
129
- fastapi_rtk-1.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
130
- fastapi_rtk-1.0.3.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
131
- fastapi_rtk-1.0.3.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
132
- fastapi_rtk-1.0.3.dist-info/RECORD,,
129
+ fastapi_rtk-1.0.5.dist-info/METADATA,sha256=4qQ4mOrd3rvE3SU3590DJYKggS00IhLaCPq8DGF8ZPI,1270
130
+ fastapi_rtk-1.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
+ fastapi_rtk-1.0.5.dist-info/entry_points.txt,sha256=UuTkxSVIokSlVN28TMhoxzRRUaPxlVRSH3Gsx6yip60,53
132
+ fastapi_rtk-1.0.5.dist-info/licenses/LICENSE,sha256=NDrWi4Qwcxal3u1r2lBWGA6TVh3OeW7yMan098mQz98,1073
133
+ fastapi_rtk-1.0.5.dist-info/RECORD,,