ExcelAlchemy 2.1.0__tar.gz → 2.2.2__tar.gz
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.
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/PKG-INFO +1 -1
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/__init__.py +1 -1
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/config.py +188 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/alchemy.py +7 -1
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/import_session.py +115 -18
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/helper/pydantic.py +13 -17
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/metadata.py +51 -39
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/LICENSE +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/README-pypi.md +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/pyproject.toml +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/base.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/boolean.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/date.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/date_range.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/multi_checkbox.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/number.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/number_range.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/organization.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/phone_number.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/radio.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/staff.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/tree.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/codecs/url.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/headers.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/rows.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/schema.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/core/writer.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/exceptions.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/i18n/messages.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/results.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/util/converter.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/util/convertor.py +0 -0
- {excelalchemy-2.1.0 → excelalchemy-2.2.2}/src/excelalchemy/util/file.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""A Python Library for Reading and Writing Excel Files"""
|
|
2
2
|
|
|
3
|
-
__version__ = '2.
|
|
3
|
+
__version__ = '2.2.2'
|
|
4
4
|
from excelalchemy._primitives.constants import CharacterSet, DataRangeOption, DateFormat, Option
|
|
5
5
|
from excelalchemy._primitives.deprecation import ExcelAlchemyDeprecationWarning
|
|
6
6
|
from excelalchemy._primitives.identity import (
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import warnings
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from enum import StrEnum
|
|
@@ -9,6 +10,7 @@ from typing import TYPE_CHECKING, Self
|
|
|
9
10
|
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
13
|
+
from excelalchemy._primitives.deprecation import DEPRECATION_REMOVAL_VERSION, ExcelAlchemyDeprecationWarning
|
|
12
14
|
from excelalchemy._primitives.payloads import DataConverter, DmlCallback, ExistenceCheckCallback, ImportContext
|
|
13
15
|
from excelalchemy.core.storage_protocol import ExcelStorage
|
|
14
16
|
from excelalchemy.exceptions import ConfigError
|
|
@@ -21,6 +23,9 @@ if TYPE_CHECKING:
|
|
|
21
23
|
from minio import Minio
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
_EMITTED_STORAGE_DEPRECATION_WARNINGS: set[bool] = set()
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
class ExcelMode(StrEnum):
|
|
25
30
|
"""Top-level Excel workflow mode."""
|
|
26
31
|
|
|
@@ -51,6 +56,10 @@ class StorageOptions:
|
|
|
51
56
|
def has_legacy_minio(self) -> bool:
|
|
52
57
|
return self.minio is not None
|
|
53
58
|
|
|
59
|
+
@property
|
|
60
|
+
def uses_legacy_minio_path(self) -> bool:
|
|
61
|
+
return self.storage is None and self.minio is not None
|
|
62
|
+
|
|
54
63
|
|
|
55
64
|
@dataclass(slots=True, frozen=True)
|
|
56
65
|
class ImporterSchemaOptions[ImportCreateModelT: BaseModel, ImportUpdateModelT: BaseModel]:
|
|
@@ -118,6 +127,116 @@ class ImporterConfig[ContextT, ImportCreateModelT: BaseModel, ImportUpdateModelT
|
|
|
118
127
|
behavior: ImportBehavior[ContextT] = field(init=False, repr=False)
|
|
119
128
|
storage_options: StorageOptions = field(init=False, repr=False)
|
|
120
129
|
|
|
130
|
+
@classmethod
|
|
131
|
+
def for_create(
|
|
132
|
+
cls,
|
|
133
|
+
importer_model: type[ImportCreateModelT],
|
|
134
|
+
*,
|
|
135
|
+
data_converter: DataConverter | None = import_data_converter,
|
|
136
|
+
creator: DmlCallback[ContextT] | None = None,
|
|
137
|
+
updater: DmlCallback[ContextT] | None = None,
|
|
138
|
+
context: ImportContext[ContextT] = None,
|
|
139
|
+
is_data_exist: ExistenceCheckCallback[ContextT] | None = None,
|
|
140
|
+
exec_formatter: Callable[[Exception], str] = str,
|
|
141
|
+
storage: ExcelStorage | None = None,
|
|
142
|
+
minio: Minio | None = None,
|
|
143
|
+
bucket_name: str = 'excel',
|
|
144
|
+
url_expires: int = 3600,
|
|
145
|
+
locale: str = 'zh-CN',
|
|
146
|
+
sheet_name: str = 'Sheet1',
|
|
147
|
+
) -> Self:
|
|
148
|
+
"""Build a create-mode importer config through the recommended constructor."""
|
|
149
|
+
return cls(
|
|
150
|
+
create_importer_model=importer_model,
|
|
151
|
+
data_converter=data_converter,
|
|
152
|
+
creator=creator,
|
|
153
|
+
updater=updater,
|
|
154
|
+
context=context,
|
|
155
|
+
is_data_exist=is_data_exist,
|
|
156
|
+
exec_formatter=exec_formatter,
|
|
157
|
+
import_mode=ImportMode.CREATE,
|
|
158
|
+
storage=storage,
|
|
159
|
+
minio=minio,
|
|
160
|
+
bucket_name=bucket_name,
|
|
161
|
+
url_expires=url_expires,
|
|
162
|
+
locale=locale,
|
|
163
|
+
sheet_name=sheet_name,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def for_update(
|
|
168
|
+
cls,
|
|
169
|
+
importer_model: type[ImportUpdateModelT],
|
|
170
|
+
*,
|
|
171
|
+
data_converter: DataConverter | None = import_data_converter,
|
|
172
|
+
creator: DmlCallback[ContextT] | None = None,
|
|
173
|
+
updater: DmlCallback[ContextT] | None = None,
|
|
174
|
+
context: ImportContext[ContextT] = None,
|
|
175
|
+
is_data_exist: ExistenceCheckCallback[ContextT] | None = None,
|
|
176
|
+
exec_formatter: Callable[[Exception], str] = str,
|
|
177
|
+
storage: ExcelStorage | None = None,
|
|
178
|
+
minio: Minio | None = None,
|
|
179
|
+
bucket_name: str = 'excel',
|
|
180
|
+
url_expires: int = 3600,
|
|
181
|
+
locale: str = 'zh-CN',
|
|
182
|
+
sheet_name: str = 'Sheet1',
|
|
183
|
+
) -> Self:
|
|
184
|
+
"""Build an update-mode importer config through the recommended constructor."""
|
|
185
|
+
return cls(
|
|
186
|
+
update_importer_model=importer_model,
|
|
187
|
+
data_converter=data_converter,
|
|
188
|
+
creator=creator,
|
|
189
|
+
updater=updater,
|
|
190
|
+
context=context,
|
|
191
|
+
is_data_exist=is_data_exist,
|
|
192
|
+
exec_formatter=exec_formatter,
|
|
193
|
+
import_mode=ImportMode.UPDATE,
|
|
194
|
+
storage=storage,
|
|
195
|
+
minio=minio,
|
|
196
|
+
bucket_name=bucket_name,
|
|
197
|
+
url_expires=url_expires,
|
|
198
|
+
locale=locale,
|
|
199
|
+
sheet_name=sheet_name,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def for_create_or_update(
|
|
204
|
+
cls,
|
|
205
|
+
*,
|
|
206
|
+
create_importer_model: type[ImportCreateModelT],
|
|
207
|
+
update_importer_model: type[ImportUpdateModelT],
|
|
208
|
+
is_data_exist: ExistenceCheckCallback[ContextT],
|
|
209
|
+
data_converter: DataConverter | None = import_data_converter,
|
|
210
|
+
creator: DmlCallback[ContextT] | None = None,
|
|
211
|
+
updater: DmlCallback[ContextT] | None = None,
|
|
212
|
+
context: ImportContext[ContextT] = None,
|
|
213
|
+
exec_formatter: Callable[[Exception], str] = str,
|
|
214
|
+
storage: ExcelStorage | None = None,
|
|
215
|
+
minio: Minio | None = None,
|
|
216
|
+
bucket_name: str = 'excel',
|
|
217
|
+
url_expires: int = 3600,
|
|
218
|
+
locale: str = 'zh-CN',
|
|
219
|
+
sheet_name: str = 'Sheet1',
|
|
220
|
+
) -> Self:
|
|
221
|
+
"""Build a create-or-update importer config through the recommended constructor."""
|
|
222
|
+
return cls(
|
|
223
|
+
create_importer_model=create_importer_model,
|
|
224
|
+
update_importer_model=update_importer_model,
|
|
225
|
+
data_converter=data_converter,
|
|
226
|
+
creator=creator,
|
|
227
|
+
updater=updater,
|
|
228
|
+
context=context,
|
|
229
|
+
is_data_exist=is_data_exist,
|
|
230
|
+
exec_formatter=exec_formatter,
|
|
231
|
+
import_mode=ImportMode.CREATE_OR_UPDATE,
|
|
232
|
+
storage=storage,
|
|
233
|
+
minio=minio,
|
|
234
|
+
bucket_name=bucket_name,
|
|
235
|
+
url_expires=url_expires,
|
|
236
|
+
locale=locale,
|
|
237
|
+
sheet_name=sheet_name,
|
|
238
|
+
)
|
|
239
|
+
|
|
121
240
|
def validate_model(self) -> Self:
|
|
122
241
|
if self.import_mode not in ImportMode.__members__.values():
|
|
123
242
|
raise ConfigError(msg(MessageKey.INVALID_IMPORT_MODE, import_mode=self.import_mode))
|
|
@@ -181,6 +300,8 @@ class ImporterConfig[ContextT, ImportCreateModelT: BaseModel, ImportUpdateModelT
|
|
|
181
300
|
bucket_name=self.bucket_name,
|
|
182
301
|
url_expires=self.url_expires,
|
|
183
302
|
)
|
|
303
|
+
if self.storage_options.has_legacy_minio:
|
|
304
|
+
_warn_legacy_storage_path(has_explicit_storage=self.storage_options.has_explicit_storage)
|
|
184
305
|
|
|
185
306
|
|
|
186
307
|
@dataclass(slots=True)
|
|
@@ -200,6 +321,50 @@ class ExporterConfig[ExportModelT: BaseModel]:
|
|
|
200
321
|
behavior: ExportBehavior = field(init=False, repr=False)
|
|
201
322
|
storage_options: StorageOptions = field(init=False, repr=False)
|
|
202
323
|
|
|
324
|
+
@classmethod
|
|
325
|
+
def for_model(
|
|
326
|
+
cls,
|
|
327
|
+
exporter_model: type[ExportModelT],
|
|
328
|
+
*,
|
|
329
|
+
data_converter: DataConverter | None = export_data_converter,
|
|
330
|
+
storage: ExcelStorage | None = None,
|
|
331
|
+
minio: Minio | None = None,
|
|
332
|
+
bucket_name: str = 'excel',
|
|
333
|
+
url_expires: int = 3600,
|
|
334
|
+
locale: str = 'zh-CN',
|
|
335
|
+
sheet_name: str = 'Sheet1',
|
|
336
|
+
) -> Self:
|
|
337
|
+
"""Build an exporter config through the recommended constructor."""
|
|
338
|
+
return cls(
|
|
339
|
+
exporter_model=exporter_model,
|
|
340
|
+
data_converter=data_converter,
|
|
341
|
+
storage=storage,
|
|
342
|
+
minio=minio,
|
|
343
|
+
bucket_name=bucket_name,
|
|
344
|
+
url_expires=url_expires,
|
|
345
|
+
locale=locale,
|
|
346
|
+
sheet_name=sheet_name,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def for_storage(
|
|
351
|
+
cls,
|
|
352
|
+
exporter_model: type[ExportModelT],
|
|
353
|
+
*,
|
|
354
|
+
storage: ExcelStorage,
|
|
355
|
+
data_converter: DataConverter | None = export_data_converter,
|
|
356
|
+
locale: str = 'zh-CN',
|
|
357
|
+
sheet_name: str = 'Sheet1',
|
|
358
|
+
) -> Self:
|
|
359
|
+
"""Build an exporter config for the recommended explicit-storage path."""
|
|
360
|
+
return cls.for_model(
|
|
361
|
+
exporter_model,
|
|
362
|
+
data_converter=data_converter,
|
|
363
|
+
storage=storage,
|
|
364
|
+
locale=locale,
|
|
365
|
+
sheet_name=sheet_name,
|
|
366
|
+
)
|
|
367
|
+
|
|
203
368
|
def validate_model(self) -> Self:
|
|
204
369
|
if not self.exporter_model:
|
|
205
370
|
raise ValueError(msg(MessageKey.EXPORTER_MODEL_CANNOT_BE_EMPTY))
|
|
@@ -219,3 +384,26 @@ class ExporterConfig[ExportModelT: BaseModel]:
|
|
|
219
384
|
bucket_name=self.bucket_name,
|
|
220
385
|
url_expires=self.url_expires,
|
|
221
386
|
)
|
|
387
|
+
if self.storage_options.has_legacy_minio:
|
|
388
|
+
_warn_legacy_storage_path(has_explicit_storage=self.storage_options.has_explicit_storage)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _warn_legacy_storage_path(*, has_explicit_storage: bool) -> None:
|
|
392
|
+
"""Emit a deprecation warning for the legacy built-in Minio config path."""
|
|
393
|
+
if has_explicit_storage in _EMITTED_STORAGE_DEPRECATION_WARNINGS:
|
|
394
|
+
return
|
|
395
|
+
_EMITTED_STORAGE_DEPRECATION_WARNINGS.add(has_explicit_storage)
|
|
396
|
+
|
|
397
|
+
detail = (
|
|
398
|
+
' The explicit `storage=` backend will be used.'
|
|
399
|
+
if has_explicit_storage
|
|
400
|
+
else ' Prefer passing `storage=` with `MinioStorageGateway` or a custom `ExcelStorage` implementation.'
|
|
401
|
+
)
|
|
402
|
+
warnings.warn(
|
|
403
|
+
(
|
|
404
|
+
'`minio`, `bucket_name`, and `url_expires` are deprecated configuration fields and will be removed in '
|
|
405
|
+
f'ExcelAlchemy {DEPRECATION_REMOVAL_VERSION}.{detail}'
|
|
406
|
+
),
|
|
407
|
+
category=ExcelAlchemyDeprecationWarning,
|
|
408
|
+
stacklevel=3,
|
|
409
|
+
)
|
|
@@ -16,7 +16,7 @@ from excelalchemy.codecs.base import SystemReserved
|
|
|
16
16
|
from excelalchemy.config import ExcelMode, ExporterConfig, ImporterConfig, ImportMode
|
|
17
17
|
from excelalchemy.core.abstract import ABCExcelAlchemy
|
|
18
18
|
from excelalchemy.core.headers import ExcelHeaderParser, ExcelHeaderValidator
|
|
19
|
-
from excelalchemy.core.import_session import ImportSession, build_import_result_field_meta
|
|
19
|
+
from excelalchemy.core.import_session import ImportSession, ImportSessionSnapshot, build_import_result_field_meta
|
|
20
20
|
from excelalchemy.core.rendering import ExcelRenderer
|
|
21
21
|
from excelalchemy.core.schema import ExcelSchemaLayout
|
|
22
22
|
from excelalchemy.core.storage import build_storage_gateway
|
|
@@ -217,6 +217,12 @@ class ExcelAlchemy[
|
|
|
217
217
|
return {}
|
|
218
218
|
return self._last_import_session.row_errors
|
|
219
219
|
|
|
220
|
+
@property
|
|
221
|
+
def last_import_snapshot(self) -> ImportSessionSnapshot | None:
|
|
222
|
+
if self._last_import_session is None:
|
|
223
|
+
return None
|
|
224
|
+
return self._last_import_session.snapshot
|
|
225
|
+
|
|
220
226
|
@property
|
|
221
227
|
def input_excel_has_merged_header(self) -> bool:
|
|
222
228
|
return self._require_last_import_session().input_excel_has_merged_header
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from enum import StrEnum
|
|
5
7
|
from functools import cached_property
|
|
6
8
|
from typing import cast
|
|
7
9
|
|
|
@@ -30,6 +32,34 @@ from excelalchemy.results import ImportResult, ValidateHeaderResult, ValidateRes
|
|
|
30
32
|
HEADER_HINT_LINE_COUNT = 1
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
class ImportSessionPhase(StrEnum):
|
|
36
|
+
"""High-level lifecycle phase for a one-shot import session."""
|
|
37
|
+
|
|
38
|
+
INITIALIZED = 'INITIALIZED'
|
|
39
|
+
WORKBOOK_LOADED = 'WORKBOOK_LOADED'
|
|
40
|
+
HEADERS_VALIDATED = 'HEADERS_VALIDATED'
|
|
41
|
+
ROWS_PREPARED = 'ROWS_PREPARED'
|
|
42
|
+
ROWS_EXECUTED = 'ROWS_EXECUTED'
|
|
43
|
+
RESULT_RENDERED = 'RESULT_RENDERED'
|
|
44
|
+
COMPLETED = 'COMPLETED'
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True, frozen=True)
|
|
48
|
+
class ImportSessionSnapshot:
|
|
49
|
+
"""Immutable snapshot of the current session lifecycle state."""
|
|
50
|
+
|
|
51
|
+
phase: ImportSessionPhase = ImportSessionPhase.INITIALIZED
|
|
52
|
+
input_excel_name: str | None = None
|
|
53
|
+
output_excel_name: str | None = None
|
|
54
|
+
has_merged_header: bool | None = None
|
|
55
|
+
data_row_count: int = 0
|
|
56
|
+
processed_row_count: int = 0
|
|
57
|
+
success_count: int = 0
|
|
58
|
+
fail_count: int = 0
|
|
59
|
+
rendered_result_workbook: bool = False
|
|
60
|
+
result: ValidateResult | None = None
|
|
61
|
+
|
|
62
|
+
|
|
33
63
|
class ImportSession[
|
|
34
64
|
ContextT,
|
|
35
65
|
ImportCreateModelT: BaseModel,
|
|
@@ -71,6 +101,7 @@ class ImportSession[
|
|
|
71
101
|
self.issue_tracker = ImportIssueTracker(self.layout, self.import_result_field_meta)
|
|
72
102
|
self.row_aggregator = RowAggregator(self.layout, self.behavior.import_mode)
|
|
73
103
|
self.executor = ImportExecutor(self.config, self.issue_tracker, lambda: self.context)
|
|
104
|
+
self._snapshot = ImportSessionSnapshot()
|
|
74
105
|
|
|
75
106
|
@property
|
|
76
107
|
def cell_errors(self):
|
|
@@ -80,6 +111,10 @@ class ImportSession[
|
|
|
80
111
|
def row_errors(self):
|
|
81
112
|
return self.issue_tracker.row_errors
|
|
82
113
|
|
|
114
|
+
@property
|
|
115
|
+
def snapshot(self) -> ImportSessionSnapshot:
|
|
116
|
+
return self._snapshot
|
|
117
|
+
|
|
83
118
|
@cached_property
|
|
84
119
|
def input_excel_has_merged_header(self) -> bool:
|
|
85
120
|
if not self._state_df_has_been_loaded:
|
|
@@ -101,42 +136,73 @@ class ImportSession[
|
|
|
101
136
|
|
|
102
137
|
async def run(self, input_excel_name: str, output_excel_name: str) -> ImportResult:
|
|
103
138
|
with use_display_locale(self.locale):
|
|
139
|
+
self._snapshot = replace(
|
|
140
|
+
self._snapshot,
|
|
141
|
+
phase=ImportSessionPhase.INITIALIZED,
|
|
142
|
+
input_excel_name=input_excel_name,
|
|
143
|
+
output_excel_name=output_excel_name,
|
|
144
|
+
rendered_result_workbook=False,
|
|
145
|
+
result=None,
|
|
146
|
+
data_row_count=0,
|
|
147
|
+
processed_row_count=0,
|
|
148
|
+
success_count=0,
|
|
149
|
+
fail_count=0,
|
|
150
|
+
)
|
|
151
|
+
|
|
104
152
|
validate_header = self._validate_header(input_excel_name)
|
|
105
153
|
if not validate_header.is_valid:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
all_success, success_count, fail_count = True, 0, 0
|
|
113
|
-
for table_row_index in range(self.extra_header_count_on_import, len(self.worksheet_table)):
|
|
114
|
-
row = self.worksheet_table.row_at(table_row_index)
|
|
115
|
-
aggregate_data = self._aggregate_data(cast(FlatRowPayload, row.to_dict()))
|
|
116
|
-
success = await self.executor.execute(RowIndex(table_row_index), aggregate_data, self.worksheet_table)
|
|
117
|
-
all_success = all_success and success
|
|
118
|
-
success_count, fail_count = (
|
|
119
|
-
(success_count + 1, fail_count) if success else (success_count, fail_count + 1)
|
|
154
|
+
header_result = ImportResult.from_validate_header_result(validate_header)
|
|
155
|
+
self._snapshot = replace(
|
|
156
|
+
self._snapshot,
|
|
157
|
+
phase=ImportSessionPhase.COMPLETED,
|
|
158
|
+
has_merged_header=self.input_excel_has_merged_header,
|
|
159
|
+
result=header_result.result,
|
|
120
160
|
)
|
|
161
|
+
return header_result
|
|
162
|
+
|
|
163
|
+
self._prepare_rows_for_execution()
|
|
164
|
+
|
|
165
|
+
all_success, success_count, fail_count = await self._execute_rows()
|
|
121
166
|
|
|
122
167
|
url = None
|
|
123
168
|
if not all_success:
|
|
124
169
|
self._add_result_column()
|
|
125
170
|
content_with_prefix = self._render_import_result_excel()
|
|
126
171
|
url = self._upload_file(output_excel_name, content_with_prefix)
|
|
172
|
+
self._snapshot = replace(
|
|
173
|
+
self._snapshot,
|
|
174
|
+
phase=ImportSessionPhase.RESULT_RENDERED,
|
|
175
|
+
rendered_result_workbook=True,
|
|
176
|
+
)
|
|
127
177
|
|
|
128
|
-
|
|
178
|
+
import_result = ImportResult(
|
|
129
179
|
result=(ValidateResult.DATA_INVALID, ValidateResult.SUCCESS)[int(all_success)],
|
|
130
180
|
url=url,
|
|
131
181
|
success_count=success_count,
|
|
132
182
|
fail_count=fail_count,
|
|
133
183
|
)
|
|
184
|
+
self._snapshot = replace(
|
|
185
|
+
self._snapshot,
|
|
186
|
+
phase=ImportSessionPhase.COMPLETED,
|
|
187
|
+
success_count=success_count,
|
|
188
|
+
fail_count=fail_count,
|
|
189
|
+
result=import_result.result,
|
|
190
|
+
)
|
|
191
|
+
return import_result
|
|
134
192
|
|
|
135
193
|
def _validate_header(self, input_excel_name: str) -> ValidateHeaderResult:
|
|
136
|
-
self.
|
|
137
|
-
|
|
194
|
+
self._load_workbook(input_excel_name)
|
|
195
|
+
validate_header = self.header_validator.validate(
|
|
196
|
+
self.input_excel_headers, self.layout, self.behavior.import_mode
|
|
197
|
+
)
|
|
198
|
+
self._snapshot = replace(
|
|
199
|
+
self._snapshot,
|
|
200
|
+
phase=ImportSessionPhase.HEADERS_VALIDATED,
|
|
201
|
+
has_merged_header=self.input_excel_has_merged_header,
|
|
202
|
+
)
|
|
203
|
+
return validate_header
|
|
138
204
|
|
|
139
|
-
def
|
|
205
|
+
def _load_workbook(self, input_excel_name: str) -> WorksheetTable:
|
|
140
206
|
if not self._state_df_has_been_loaded:
|
|
141
207
|
worksheet_table = self.storage_gateway.read_excel_table(
|
|
142
208
|
input_excel_name,
|
|
@@ -146,8 +212,39 @@ class ImportSession[
|
|
|
146
212
|
self.worksheet_table = worksheet_table
|
|
147
213
|
self.header_table = worksheet_table.head(2)
|
|
148
214
|
self._state_df_has_been_loaded = True
|
|
215
|
+
self._snapshot = replace(self._snapshot, phase=ImportSessionPhase.WORKBOOK_LOADED)
|
|
149
216
|
return self.worksheet_table
|
|
150
217
|
|
|
218
|
+
def _prepare_rows_for_execution(self) -> None:
|
|
219
|
+
self.worksheet_table = self.worksheet_table.iloc[1:]
|
|
220
|
+
self._set_columns(self.worksheet_table)
|
|
221
|
+
self.worksheet_table = self.worksheet_table.reset_index(drop=True)
|
|
222
|
+
self._snapshot = replace(
|
|
223
|
+
self._snapshot,
|
|
224
|
+
phase=ImportSessionPhase.ROWS_PREPARED,
|
|
225
|
+
data_row_count=max(0, len(self.worksheet_table) - self.extra_header_count_on_import),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def _execute_rows(self) -> tuple[bool, int, int]:
|
|
229
|
+
all_success, success_count, fail_count = True, 0, 0
|
|
230
|
+
processed_row_count = 0
|
|
231
|
+
for table_row_index in range(self.extra_header_count_on_import, len(self.worksheet_table)):
|
|
232
|
+
row = self.worksheet_table.row_at(table_row_index)
|
|
233
|
+
aggregate_data = self._aggregate_data(cast(FlatRowPayload, row.to_dict()))
|
|
234
|
+
success = await self.executor.execute(RowIndex(table_row_index), aggregate_data, self.worksheet_table)
|
|
235
|
+
processed_row_count += 1
|
|
236
|
+
all_success = all_success and success
|
|
237
|
+
success_count, fail_count = (success_count + 1, fail_count) if success else (success_count, fail_count + 1)
|
|
238
|
+
|
|
239
|
+
self._snapshot = replace(
|
|
240
|
+
self._snapshot,
|
|
241
|
+
phase=ImportSessionPhase.ROWS_EXECUTED,
|
|
242
|
+
processed_row_count=processed_row_count,
|
|
243
|
+
success_count=success_count,
|
|
244
|
+
fail_count=fail_count,
|
|
245
|
+
)
|
|
246
|
+
return all_success, success_count, fail_count
|
|
247
|
+
|
|
151
248
|
def _set_columns(self, worksheet_table: WorksheetTable) -> WorksheetTable:
|
|
152
249
|
return self.header_parser.apply_columns(
|
|
153
250
|
worksheet_table,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import Generator, Iterable, Mapping
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from types import UnionType
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Union, cast, get_args, get_origin
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, ValidationError
|
|
7
7
|
from pydantic.fields import FieldInfo
|
|
@@ -23,23 +23,23 @@ class PydanticFieldAdapter:
|
|
|
23
23
|
raw_field: FieldInfo
|
|
24
24
|
|
|
25
25
|
@property
|
|
26
|
-
def annotation(self) ->
|
|
26
|
+
def annotation(self) -> object:
|
|
27
27
|
return self.raw_field.annotation
|
|
28
28
|
|
|
29
29
|
@property
|
|
30
|
-
def excel_codec(self) -> type[
|
|
30
|
+
def excel_codec(self) -> type[ExcelFieldCodec]:
|
|
31
31
|
annotation = self.annotation
|
|
32
32
|
origin = get_origin(annotation)
|
|
33
33
|
if origin in (UnionType, Union):
|
|
34
34
|
args = [arg for arg in get_args(annotation) if arg is not type(None)]
|
|
35
35
|
if len(args) != 1:
|
|
36
36
|
raise ProgrammaticError(msg(MessageKey.UNSUPPORTED_FIELD_TYPE_DECLARATION, annotation=annotation))
|
|
37
|
-
return cast(type[
|
|
37
|
+
return cast(type[ExcelFieldCodec], args[0])
|
|
38
38
|
|
|
39
|
-
return cast(type[
|
|
39
|
+
return cast(type[ExcelFieldCodec], annotation)
|
|
40
40
|
|
|
41
41
|
@property
|
|
42
|
-
def value_type(self) -> type[
|
|
42
|
+
def value_type(self) -> type[ExcelFieldCodec]:
|
|
43
43
|
"""Backward-compatible alias for excel_codec."""
|
|
44
44
|
return self.excel_codec
|
|
45
45
|
|
|
@@ -67,14 +67,14 @@ class PydanticFieldAdapter:
|
|
|
67
67
|
declared = self.declared_metadata
|
|
68
68
|
return declared.bind_runtime(
|
|
69
69
|
required=self.required,
|
|
70
|
-
excel_codec=
|
|
70
|
+
excel_codec=self.excel_codec,
|
|
71
71
|
parent_label=declared.label,
|
|
72
72
|
parent_key=Key(self.name),
|
|
73
73
|
key=Key(self.name),
|
|
74
74
|
offset=0,
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
-
def validate_value(self, raw_value:
|
|
77
|
+
def validate_value(self, raw_value: object) -> object:
|
|
78
78
|
if raw_value is None:
|
|
79
79
|
if self.allows_none and not self.required:
|
|
80
80
|
return None
|
|
@@ -116,12 +116,12 @@ def get_model_field_names(model: type[BaseModel]) -> list[str]:
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def instantiate_pydantic_model[ModelT: BaseModel](
|
|
119
|
-
data: Mapping[str,
|
|
119
|
+
data: Mapping[str, object],
|
|
120
120
|
model: type[ModelT],
|
|
121
121
|
) -> ModelT | list[ExcelCellError | ExcelRowError]:
|
|
122
122
|
"""Instantiate a Pydantic model and return mapped Excel errors when validation fails."""
|
|
123
123
|
model_adapter = PydanticModelAdapter(model)
|
|
124
|
-
normalized_data: dict[str,
|
|
124
|
+
normalized_data: dict[str, object] = {}
|
|
125
125
|
errors: list[ExcelCellError | ExcelRowError] = []
|
|
126
126
|
failed_fields: set[str] = set()
|
|
127
127
|
|
|
@@ -158,18 +158,14 @@ def _extract_pydantic_model(model: PydanticModelAdapter) -> Generator[FieldMetaI
|
|
|
158
158
|
inherited = sub_field_info.inherited_from(declared_metadata)
|
|
159
159
|
yield inherited.bind_runtime(
|
|
160
160
|
required=field_adapter.required,
|
|
161
|
-
excel_codec=
|
|
161
|
+
excel_codec=excel_codec,
|
|
162
162
|
parent_label=declared_metadata.label,
|
|
163
163
|
parent_key=Key(field_adapter.name),
|
|
164
164
|
key=key,
|
|
165
165
|
offset=offset,
|
|
166
166
|
)
|
|
167
|
-
|
|
168
|
-
elif issubclass(excel_codec, ExcelFieldCodec):
|
|
169
|
-
yield field_adapter.runtime_metadata()
|
|
170
|
-
|
|
171
167
|
else:
|
|
172
|
-
|
|
168
|
+
yield field_adapter.runtime_metadata()
|
|
173
169
|
|
|
174
170
|
|
|
175
171
|
def _handle_error(
|
|
@@ -188,7 +184,7 @@ def _handle_error(
|
|
|
188
184
|
|
|
189
185
|
|
|
190
186
|
def _model_validate[ModelT: BaseModel](
|
|
191
|
-
data: dict[str,
|
|
187
|
+
data: dict[str, object],
|
|
192
188
|
model: type[ModelT],
|
|
193
189
|
model_adapter: PydanticModelAdapter,
|
|
194
190
|
failed_fields: set[str],
|
|
@@ -4,7 +4,7 @@ import copy
|
|
|
4
4
|
import datetime
|
|
5
5
|
import logging
|
|
6
6
|
from collections.abc import Callable, Mapping, Set
|
|
7
|
-
from dataclasses import dataclass, field
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
8
|
from functools import cached_property
|
|
9
9
|
from typing import Any, Self, cast
|
|
10
10
|
|
|
@@ -37,6 +37,16 @@ type FieldDefaultFactory = Callable[[], object]
|
|
|
37
37
|
type FieldIncludeExclude = Set[IntStr] | bool | None
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def _normalize_character_set(character_set: set[CharacterSet] | None) -> frozenset[CharacterSet]:
|
|
41
|
+
return frozenset(character_set or set(CharacterSet))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _normalize_options(options: list[Option] | tuple[Option, ...] | None) -> tuple[Option, ...] | None:
|
|
45
|
+
if options is None:
|
|
46
|
+
return None
|
|
47
|
+
return tuple(options)
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
class PatchFieldMeta(BaseModel):
|
|
41
51
|
unique: bool | None = False # Workbook hint only. Runtime uniqueness is enforced elsewhere.
|
|
42
52
|
is_primary_key: bool | None = False # Workbook hint only. Runtime primary-key behavior is configured separately.
|
|
@@ -44,7 +54,7 @@ class PatchFieldMeta(BaseModel):
|
|
|
44
54
|
options: list[Option] | None = None
|
|
45
55
|
|
|
46
56
|
|
|
47
|
-
@dataclass(slots=True)
|
|
57
|
+
@dataclass(slots=True, frozen=True)
|
|
48
58
|
class DeclaredFieldMeta:
|
|
49
59
|
"""Static workbook field declaration supplied by user code."""
|
|
50
60
|
|
|
@@ -56,7 +66,7 @@ class DeclaredFieldMeta:
|
|
|
56
66
|
order: int
|
|
57
67
|
|
|
58
68
|
|
|
59
|
-
@dataclass(slots=True)
|
|
69
|
+
@dataclass(slots=True, frozen=True)
|
|
60
70
|
class RuntimeFieldBinding:
|
|
61
71
|
"""Runtime identity assigned after schema extraction flattens the model."""
|
|
62
72
|
|
|
@@ -67,21 +77,21 @@ class RuntimeFieldBinding:
|
|
|
67
77
|
excel_codec: type[ExcelFieldCodec] = UndefinedFieldCodec
|
|
68
78
|
|
|
69
79
|
|
|
70
|
-
@dataclass(slots=True)
|
|
80
|
+
@dataclass(slots=True, frozen=True)
|
|
71
81
|
class WorkbookPresentationMeta:
|
|
72
82
|
"""Workbook-facing comment and formatting metadata."""
|
|
73
83
|
|
|
74
|
-
character_set:
|
|
84
|
+
character_set: frozenset[CharacterSet] = field(default_factory=lambda: frozenset(CharacterSet))
|
|
75
85
|
fraction_digits: int | None = None
|
|
76
86
|
timezone: datetime.timezone = field(default_factory=lambda: datetime.timezone(datetime.timedelta(hours=8), 'CST'))
|
|
77
87
|
date_format: DateFormat | None = None
|
|
78
88
|
date_range_option: DataRangeOption | None = None
|
|
79
|
-
options:
|
|
89
|
+
options: tuple[Option, ...] | None = None
|
|
80
90
|
unit: str | None = None
|
|
81
91
|
hint: str | None = None
|
|
82
92
|
|
|
83
93
|
|
|
84
|
-
@dataclass(slots=True)
|
|
94
|
+
@dataclass(slots=True, frozen=True)
|
|
85
95
|
class ImportConstraints:
|
|
86
96
|
"""Importer-side validation hints mirrored from Pydantic constraints."""
|
|
87
97
|
|
|
@@ -136,12 +146,12 @@ class FieldMetaInfo:
|
|
|
136
146
|
)
|
|
137
147
|
self.runtime_binding = RuntimeFieldBinding()
|
|
138
148
|
self.presentation_meta = WorkbookPresentationMeta(
|
|
139
|
-
character_set=character_set
|
|
149
|
+
character_set=_normalize_character_set(character_set),
|
|
140
150
|
fraction_digits=fraction_digits,
|
|
141
151
|
timezone=timezone or datetime.timezone(datetime.timedelta(hours=8), 'CST'),
|
|
142
152
|
date_format=date_format,
|
|
143
153
|
date_range_option=date_range_option,
|
|
144
|
-
options=options,
|
|
154
|
+
options=_normalize_options(options),
|
|
145
155
|
unit=unit,
|
|
146
156
|
hint=hint,
|
|
147
157
|
)
|
|
@@ -196,7 +206,7 @@ class FieldMetaInfo:
|
|
|
196
206
|
|
|
197
207
|
@excel_codec.setter
|
|
198
208
|
def excel_codec(self, value: type[ExcelFieldCodec]) -> None:
|
|
199
|
-
self.runtime_binding.excel_codec
|
|
209
|
+
self.runtime_binding = replace(self.runtime_binding, excel_codec=value)
|
|
200
210
|
|
|
201
211
|
@property
|
|
202
212
|
def value_type(self) -> type[ExcelFieldCodec]:
|
|
@@ -383,7 +393,7 @@ class FieldMetaInfo:
|
|
|
383
393
|
|
|
384
394
|
@label.setter
|
|
385
395
|
def label(self, value: str | Label) -> None:
|
|
386
|
-
self.declared_meta.label
|
|
396
|
+
self.declared_meta = replace(self.declared_meta, label=Label(value))
|
|
387
397
|
|
|
388
398
|
@property
|
|
389
399
|
def is_primary_key(self) -> bool:
|
|
@@ -391,7 +401,7 @@ class FieldMetaInfo:
|
|
|
391
401
|
|
|
392
402
|
@is_primary_key.setter
|
|
393
403
|
def is_primary_key(self, value: bool) -> None:
|
|
394
|
-
self.declared_meta.is_primary_key
|
|
404
|
+
self.declared_meta = replace(self.declared_meta, is_primary_key=value)
|
|
395
405
|
|
|
396
406
|
@property
|
|
397
407
|
def unique(self) -> bool:
|
|
@@ -399,7 +409,7 @@ class FieldMetaInfo:
|
|
|
399
409
|
|
|
400
410
|
@unique.setter
|
|
401
411
|
def unique(self, value: bool) -> None:
|
|
402
|
-
self.declared_meta.unique
|
|
412
|
+
self.declared_meta = replace(self.declared_meta, unique=value)
|
|
403
413
|
|
|
404
414
|
@property
|
|
405
415
|
def ignore_import(self) -> bool:
|
|
@@ -407,7 +417,7 @@ class FieldMetaInfo:
|
|
|
407
417
|
|
|
408
418
|
@ignore_import.setter
|
|
409
419
|
def ignore_import(self, value: bool) -> None:
|
|
410
|
-
self.declared_meta.ignore_import
|
|
420
|
+
self.declared_meta = replace(self.declared_meta, ignore_import=value)
|
|
411
421
|
|
|
412
422
|
@property
|
|
413
423
|
def required(self) -> bool | None:
|
|
@@ -415,7 +425,7 @@ class FieldMetaInfo:
|
|
|
415
425
|
|
|
416
426
|
@required.setter
|
|
417
427
|
def required(self, value: bool | None) -> None:
|
|
418
|
-
self.declared_meta.required
|
|
428
|
+
self.declared_meta = replace(self.declared_meta, required=value)
|
|
419
429
|
|
|
420
430
|
@property
|
|
421
431
|
def order(self) -> int:
|
|
@@ -423,7 +433,7 @@ class FieldMetaInfo:
|
|
|
423
433
|
|
|
424
434
|
@order.setter
|
|
425
435
|
def order(self, value: int) -> None:
|
|
426
|
-
self.declared_meta.order
|
|
436
|
+
self.declared_meta = replace(self.declared_meta, order=value)
|
|
427
437
|
|
|
428
438
|
@property
|
|
429
439
|
def parent_label(self) -> Label | None:
|
|
@@ -431,7 +441,7 @@ class FieldMetaInfo:
|
|
|
431
441
|
|
|
432
442
|
@parent_label.setter
|
|
433
443
|
def parent_label(self, value: Label | None) -> None:
|
|
434
|
-
self.runtime_binding.parent_label
|
|
444
|
+
self.runtime_binding = replace(self.runtime_binding, parent_label=value)
|
|
435
445
|
|
|
436
446
|
@property
|
|
437
447
|
def key(self) -> Key | None:
|
|
@@ -439,7 +449,7 @@ class FieldMetaInfo:
|
|
|
439
449
|
|
|
440
450
|
@key.setter
|
|
441
451
|
def key(self, value: Key | None) -> None:
|
|
442
|
-
self.runtime_binding.key
|
|
452
|
+
self.runtime_binding = replace(self.runtime_binding, key=value)
|
|
443
453
|
|
|
444
454
|
@property
|
|
445
455
|
def parent_key(self) -> Key | None:
|
|
@@ -447,7 +457,7 @@ class FieldMetaInfo:
|
|
|
447
457
|
|
|
448
458
|
@parent_key.setter
|
|
449
459
|
def parent_key(self, value: Key | None) -> None:
|
|
450
|
-
self.runtime_binding.parent_key
|
|
460
|
+
self.runtime_binding = replace(self.runtime_binding, parent_key=value)
|
|
451
461
|
|
|
452
462
|
@property
|
|
453
463
|
def offset(self) -> int:
|
|
@@ -455,15 +465,15 @@ class FieldMetaInfo:
|
|
|
455
465
|
|
|
456
466
|
@offset.setter
|
|
457
467
|
def offset(self, value: int) -> None:
|
|
458
|
-
self.runtime_binding.offset
|
|
468
|
+
self.runtime_binding = replace(self.runtime_binding, offset=value)
|
|
459
469
|
|
|
460
470
|
@property
|
|
461
471
|
def character_set(self) -> set[CharacterSet]:
|
|
462
|
-
return self.presentation_meta.character_set
|
|
472
|
+
return set(self.presentation_meta.character_set)
|
|
463
473
|
|
|
464
474
|
@character_set.setter
|
|
465
475
|
def character_set(self, value: set[CharacterSet]) -> None:
|
|
466
|
-
self.presentation_meta.character_set
|
|
476
|
+
self.presentation_meta = replace(self.presentation_meta, character_set=_normalize_character_set(value))
|
|
467
477
|
|
|
468
478
|
@property
|
|
469
479
|
def fraction_digits(self) -> int | None:
|
|
@@ -471,7 +481,7 @@ class FieldMetaInfo:
|
|
|
471
481
|
|
|
472
482
|
@fraction_digits.setter
|
|
473
483
|
def fraction_digits(self, value: int | None) -> None:
|
|
474
|
-
self.presentation_meta.fraction_digits
|
|
484
|
+
self.presentation_meta = replace(self.presentation_meta, fraction_digits=value)
|
|
475
485
|
|
|
476
486
|
@property
|
|
477
487
|
def timezone(self) -> datetime.timezone:
|
|
@@ -479,7 +489,7 @@ class FieldMetaInfo:
|
|
|
479
489
|
|
|
480
490
|
@timezone.setter
|
|
481
491
|
def timezone(self, value: datetime.timezone) -> None:
|
|
482
|
-
self.presentation_meta.timezone
|
|
492
|
+
self.presentation_meta = replace(self.presentation_meta, timezone=value)
|
|
483
493
|
|
|
484
494
|
@property
|
|
485
495
|
def date_format(self) -> DateFormat | None:
|
|
@@ -487,7 +497,7 @@ class FieldMetaInfo:
|
|
|
487
497
|
|
|
488
498
|
@date_format.setter
|
|
489
499
|
def date_format(self, value: DateFormat | None) -> None:
|
|
490
|
-
self.presentation_meta.date_format
|
|
500
|
+
self.presentation_meta = replace(self.presentation_meta, date_format=value)
|
|
491
501
|
|
|
492
502
|
@property
|
|
493
503
|
def date_range_option(self) -> DataRangeOption | None:
|
|
@@ -495,15 +505,17 @@ class FieldMetaInfo:
|
|
|
495
505
|
|
|
496
506
|
@date_range_option.setter
|
|
497
507
|
def date_range_option(self, value: DataRangeOption | None) -> None:
|
|
498
|
-
self.presentation_meta.date_range_option
|
|
508
|
+
self.presentation_meta = replace(self.presentation_meta, date_range_option=value)
|
|
499
509
|
|
|
500
510
|
@property
|
|
501
511
|
def options(self) -> list[Option] | None:
|
|
502
|
-
|
|
512
|
+
if self.presentation_meta.options is None:
|
|
513
|
+
return None
|
|
514
|
+
return list(self.presentation_meta.options)
|
|
503
515
|
|
|
504
516
|
@options.setter
|
|
505
517
|
def options(self, value: list[Option] | None) -> None:
|
|
506
|
-
self.presentation_meta.options
|
|
518
|
+
self.presentation_meta = replace(self.presentation_meta, options=_normalize_options(value))
|
|
507
519
|
|
|
508
520
|
@property
|
|
509
521
|
def unit(self) -> str | None:
|
|
@@ -511,7 +523,7 @@ class FieldMetaInfo:
|
|
|
511
523
|
|
|
512
524
|
@unit.setter
|
|
513
525
|
def unit(self, value: str | None) -> None:
|
|
514
|
-
self.presentation_meta.unit
|
|
526
|
+
self.presentation_meta = replace(self.presentation_meta, unit=value)
|
|
515
527
|
|
|
516
528
|
@property
|
|
517
529
|
def hint(self) -> str | None:
|
|
@@ -519,7 +531,7 @@ class FieldMetaInfo:
|
|
|
519
531
|
|
|
520
532
|
@hint.setter
|
|
521
533
|
def hint(self, value: str | None) -> None:
|
|
522
|
-
self.presentation_meta.hint
|
|
534
|
+
self.presentation_meta = replace(self.presentation_meta, hint=value)
|
|
523
535
|
|
|
524
536
|
@property
|
|
525
537
|
def importer_ge(self) -> float | None:
|
|
@@ -527,7 +539,7 @@ class FieldMetaInfo:
|
|
|
527
539
|
|
|
528
540
|
@importer_ge.setter
|
|
529
541
|
def importer_ge(self, value: float | None) -> None:
|
|
530
|
-
self.import_constraints.ge
|
|
542
|
+
self.import_constraints = replace(self.import_constraints, ge=value)
|
|
531
543
|
|
|
532
544
|
@property
|
|
533
545
|
def importer_le(self) -> float | None:
|
|
@@ -535,7 +547,7 @@ class FieldMetaInfo:
|
|
|
535
547
|
|
|
536
548
|
@importer_le.setter
|
|
537
549
|
def importer_le(self, value: float | None) -> None:
|
|
538
|
-
self.import_constraints.le
|
|
550
|
+
self.import_constraints = replace(self.import_constraints, le=value)
|
|
539
551
|
|
|
540
552
|
@property
|
|
541
553
|
def importer_max_digits(self) -> int | None:
|
|
@@ -543,7 +555,7 @@ class FieldMetaInfo:
|
|
|
543
555
|
|
|
544
556
|
@importer_max_digits.setter
|
|
545
557
|
def importer_max_digits(self, value: int | None) -> None:
|
|
546
|
-
self.import_constraints.max_digits
|
|
558
|
+
self.import_constraints = replace(self.import_constraints, max_digits=value)
|
|
547
559
|
|
|
548
560
|
@property
|
|
549
561
|
def importer_decimal_places(self) -> int | None:
|
|
@@ -551,7 +563,7 @@ class FieldMetaInfo:
|
|
|
551
563
|
|
|
552
564
|
@importer_decimal_places.setter
|
|
553
565
|
def importer_decimal_places(self, value: int | None) -> None:
|
|
554
|
-
self.import_constraints.decimal_places
|
|
566
|
+
self.import_constraints = replace(self.import_constraints, decimal_places=value)
|
|
555
567
|
|
|
556
568
|
@property
|
|
557
569
|
def importer_min_length(self) -> int | None:
|
|
@@ -559,7 +571,7 @@ class FieldMetaInfo:
|
|
|
559
571
|
|
|
560
572
|
@importer_min_length.setter
|
|
561
573
|
def importer_min_length(self, value: int | None) -> None:
|
|
562
|
-
self.import_constraints.min_length
|
|
574
|
+
self.import_constraints = replace(self.import_constraints, min_length=value)
|
|
563
575
|
|
|
564
576
|
@property
|
|
565
577
|
def importer_max_length(self) -> int | None:
|
|
@@ -567,7 +579,7 @@ class FieldMetaInfo:
|
|
|
567
579
|
|
|
568
580
|
@importer_max_length.setter
|
|
569
581
|
def importer_max_length(self, value: int | None) -> None:
|
|
570
|
-
self.import_constraints.max_length
|
|
582
|
+
self.import_constraints = replace(self.import_constraints, max_length=value)
|
|
571
583
|
|
|
572
584
|
@property
|
|
573
585
|
def importer_min_items(self) -> int | None:
|
|
@@ -575,7 +587,7 @@ class FieldMetaInfo:
|
|
|
575
587
|
|
|
576
588
|
@importer_min_items.setter
|
|
577
589
|
def importer_min_items(self, value: int | None) -> None:
|
|
578
|
-
self.import_constraints.min_items
|
|
590
|
+
self.import_constraints = replace(self.import_constraints, min_items=value)
|
|
579
591
|
|
|
580
592
|
@property
|
|
581
593
|
def importer_max_items(self) -> int | None:
|
|
@@ -583,7 +595,7 @@ class FieldMetaInfo:
|
|
|
583
595
|
|
|
584
596
|
@importer_max_items.setter
|
|
585
597
|
def importer_max_items(self, value: int | None) -> None:
|
|
586
|
-
self.import_constraints.max_items
|
|
598
|
+
self.import_constraints = replace(self.import_constraints, max_items=value)
|
|
587
599
|
|
|
588
600
|
@property
|
|
589
601
|
def importer_unique_items(self) -> bool | None:
|
|
@@ -591,7 +603,7 @@ class FieldMetaInfo:
|
|
|
591
603
|
|
|
592
604
|
@importer_unique_items.setter
|
|
593
605
|
def importer_unique_items(self, value: bool | None) -> None:
|
|
594
|
-
self.import_constraints.unique_items
|
|
606
|
+
self.import_constraints = replace(self.import_constraints, unique_items=value)
|
|
595
607
|
|
|
596
608
|
|
|
597
609
|
def extract_declared_field_metadata(field_info: FieldInfo) -> FieldMetaInfo:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|