ExcelAlchemy 2.0.0.post1__tar.gz → 2.1.0__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.0.0.post1 → excelalchemy-2.1.0}/PKG-INFO +1 -1
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/__init__.py +1 -1
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/config.py +103 -7
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/abstract.py +3 -5
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/alchemy.py +115 -206
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/executor.py +40 -33
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/headers.py +19 -19
- excelalchemy-2.1.0/src/excelalchemy/core/import_session.py +210 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/rendering.py +9 -5
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/rows.py +16 -11
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/storage.py +8 -8
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/storage_minio.py +12 -11
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/writer.py +35 -35
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/metadata.py +302 -36
- excelalchemy-2.1.0/src/excelalchemy/util/converter.py +50 -0
- excelalchemy-2.1.0/src/excelalchemy/util/convertor.py +8 -0
- excelalchemy-2.0.0.post1/src/excelalchemy/util/convertor.py +0 -53
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/LICENSE +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/README-pypi.md +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/pyproject.toml +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/base.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/boolean.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/date.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/date_range.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/multi_checkbox.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/number.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/number_range.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/organization.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/phone_number.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/radio.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/staff.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/tree.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/codecs/url.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/schema.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/exceptions.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/helper/pydantic.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/i18n/messages.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/results.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.0.0.post1 → excelalchemy-2.1.0}/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.1.0'
|
|
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 (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
-
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
7
|
from enum import StrEnum
|
|
8
8
|
from typing import TYPE_CHECKING, Self
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ from excelalchemy.exceptions import ConfigError
|
|
|
15
15
|
from excelalchemy.helper.pydantic import get_model_field_names
|
|
16
16
|
from excelalchemy.i18n.messages import MessageKey
|
|
17
17
|
from excelalchemy.i18n.messages import message as msg
|
|
18
|
-
from excelalchemy.util.
|
|
18
|
+
from excelalchemy.util.converter import export_data_converter, import_data_converter
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from minio import Minio
|
|
@@ -34,10 +34,67 @@ class ImportMode(StrEnum):
|
|
|
34
34
|
CREATE_OR_UPDATE = 'CREATE_OR_UPDATE'
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
@dataclass(slots=True, frozen=True)
|
|
38
|
+
class StorageOptions:
|
|
39
|
+
"""Normalized storage backend settings shared by importer and exporter configs."""
|
|
40
|
+
|
|
41
|
+
storage: ExcelStorage | None
|
|
42
|
+
minio: Minio | None
|
|
43
|
+
bucket_name: str
|
|
44
|
+
url_expires: int
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def has_explicit_storage(self) -> bool:
|
|
48
|
+
return self.storage is not None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def has_legacy_minio(self) -> bool:
|
|
52
|
+
return self.minio is not None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True, frozen=True)
|
|
56
|
+
class ImporterSchemaOptions[ImportCreateModelT: BaseModel, ImportUpdateModelT: BaseModel]:
|
|
57
|
+
"""Schema declaration and workbook presentation settings for imports."""
|
|
58
|
+
|
|
59
|
+
create_importer_model: type[ImportCreateModelT] | None
|
|
60
|
+
update_importer_model: type[ImportUpdateModelT] | None
|
|
61
|
+
sheet_name: str
|
|
62
|
+
locale: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(slots=True, frozen=True)
|
|
66
|
+
class ImportBehavior[ContextT]:
|
|
67
|
+
"""Execution callbacks and import workflow policy."""
|
|
68
|
+
|
|
69
|
+
data_converter: DataConverter | None
|
|
70
|
+
creator: DmlCallback[ContextT] | None
|
|
71
|
+
updater: DmlCallback[ContextT] | None
|
|
72
|
+
context: ImportContext[ContextT]
|
|
73
|
+
is_data_exist: ExistenceCheckCallback[ContextT] | None
|
|
74
|
+
exec_formatter: Callable[[Exception], str]
|
|
75
|
+
import_mode: ImportMode
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(slots=True, frozen=True)
|
|
79
|
+
class ExporterSchemaOptions[ExportModelT: BaseModel]:
|
|
80
|
+
"""Schema declaration and workbook presentation settings for exports."""
|
|
81
|
+
|
|
82
|
+
exporter_model: type[ExportModelT]
|
|
83
|
+
sheet_name: str
|
|
84
|
+
locale: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(slots=True, frozen=True)
|
|
88
|
+
class ExportBehavior:
|
|
89
|
+
"""Execution behavior used when rendering export rows."""
|
|
90
|
+
|
|
91
|
+
data_converter: DataConverter | None
|
|
92
|
+
|
|
93
|
+
|
|
37
94
|
@dataclass(slots=True)
|
|
38
|
-
class ImporterConfig[ContextT,
|
|
39
|
-
create_importer_model: type[
|
|
40
|
-
update_importer_model: type[
|
|
95
|
+
class ImporterConfig[ContextT, ImportCreateModelT: BaseModel, ImportUpdateModelT: BaseModel]:
|
|
96
|
+
create_importer_model: type[ImportCreateModelT] | None = None
|
|
97
|
+
update_importer_model: type[ImportUpdateModelT] | None = None
|
|
41
98
|
|
|
42
99
|
# The converter receives schema keys rather than workbook labels.
|
|
43
100
|
data_converter: DataConverter | None = import_data_converter
|
|
@@ -57,6 +114,9 @@ class ImporterConfig[ContextT, ImporterCreateModelT: BaseModel, ImporterUpdateMo
|
|
|
57
114
|
locale: str = 'zh-CN'
|
|
58
115
|
|
|
59
116
|
sheet_name: str = 'Sheet1'
|
|
117
|
+
schema_options: ImporterSchemaOptions[ImportCreateModelT, ImportUpdateModelT] = field(init=False, repr=False)
|
|
118
|
+
behavior: ImportBehavior[ContextT] = field(init=False, repr=False)
|
|
119
|
+
storage_options: StorageOptions = field(init=False, repr=False)
|
|
60
120
|
|
|
61
121
|
def validate_model(self) -> Self:
|
|
62
122
|
if self.import_mode not in ImportMode.__members__.values():
|
|
@@ -100,11 +160,32 @@ class ImporterConfig[ContextT, ImporterCreateModelT: BaseModel, ImporterUpdateMo
|
|
|
100
160
|
|
|
101
161
|
def __post_init__(self) -> None:
|
|
102
162
|
self.validate_model()
|
|
163
|
+
self.schema_options = ImporterSchemaOptions(
|
|
164
|
+
create_importer_model=self.create_importer_model,
|
|
165
|
+
update_importer_model=self.update_importer_model,
|
|
166
|
+
sheet_name=self.sheet_name,
|
|
167
|
+
locale=self.locale,
|
|
168
|
+
)
|
|
169
|
+
self.behavior = ImportBehavior(
|
|
170
|
+
data_converter=self.data_converter,
|
|
171
|
+
creator=self.creator,
|
|
172
|
+
updater=self.updater,
|
|
173
|
+
context=self.context,
|
|
174
|
+
is_data_exist=self.is_data_exist,
|
|
175
|
+
exec_formatter=self.exec_formatter,
|
|
176
|
+
import_mode=self.import_mode,
|
|
177
|
+
)
|
|
178
|
+
self.storage_options = StorageOptions(
|
|
179
|
+
storage=self.storage,
|
|
180
|
+
minio=self.minio,
|
|
181
|
+
bucket_name=self.bucket_name,
|
|
182
|
+
url_expires=self.url_expires,
|
|
183
|
+
)
|
|
103
184
|
|
|
104
185
|
|
|
105
186
|
@dataclass(slots=True)
|
|
106
|
-
class ExporterConfig[
|
|
107
|
-
exporter_model: type[
|
|
187
|
+
class ExporterConfig[ExportModelT: BaseModel]:
|
|
188
|
+
exporter_model: type[ExportModelT]
|
|
108
189
|
# The converter receives schema keys rather than workbook labels.
|
|
109
190
|
data_converter: DataConverter | None = export_data_converter
|
|
110
191
|
|
|
@@ -115,6 +196,9 @@ class ExporterConfig[ExporterModelT: BaseModel]:
|
|
|
115
196
|
locale: str = 'zh-CN'
|
|
116
197
|
|
|
117
198
|
sheet_name: str = 'Sheet1'
|
|
199
|
+
schema_options: ExporterSchemaOptions[ExportModelT] = field(init=False, repr=False)
|
|
200
|
+
behavior: ExportBehavior = field(init=False, repr=False)
|
|
201
|
+
storage_options: StorageOptions = field(init=False, repr=False)
|
|
118
202
|
|
|
119
203
|
def validate_model(self) -> Self:
|
|
120
204
|
if not self.exporter_model:
|
|
@@ -123,3 +207,15 @@ class ExporterConfig[ExporterModelT: BaseModel]:
|
|
|
123
207
|
|
|
124
208
|
def __post_init__(self) -> None:
|
|
125
209
|
self.validate_model()
|
|
210
|
+
self.schema_options = ExporterSchemaOptions(
|
|
211
|
+
exporter_model=self.exporter_model,
|
|
212
|
+
sheet_name=self.sheet_name,
|
|
213
|
+
locale=self.locale,
|
|
214
|
+
)
|
|
215
|
+
self.behavior = ExportBehavior(data_converter=self.data_converter)
|
|
216
|
+
self.storage_options = StorageOptions(
|
|
217
|
+
storage=self.storage,
|
|
218
|
+
minio=self.minio,
|
|
219
|
+
bucket_name=self.bucket_name,
|
|
220
|
+
url_expires=self.url_expires,
|
|
221
|
+
)
|
|
@@ -11,11 +11,9 @@ from excelalchemy.results import ImportResult
|
|
|
11
11
|
|
|
12
12
|
class ABCExcelAlchemy[
|
|
13
13
|
ContextT,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
UpdateModelT: BaseModel,
|
|
18
|
-
ExporterModelT: BaseModel,
|
|
14
|
+
ImportCreateModelT: BaseModel,
|
|
15
|
+
ImportUpdateModelT: BaseModel,
|
|
16
|
+
ExportModelT: BaseModel,
|
|
19
17
|
](ABC):
|
|
20
18
|
@abstractmethod
|
|
21
19
|
def download_template(self, sample_data: list[ExportRowPayload] | None = None) -> DataUrlStr:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from collections.abc import
|
|
3
|
-
from functools import cached_property
|
|
2
|
+
from collections.abc import Sequence
|
|
4
3
|
from typing import cast
|
|
5
4
|
|
|
6
5
|
from pydantic import BaseModel
|
|
@@ -10,16 +9,15 @@ from excelalchemy._primitives.constants import (
|
|
|
10
9
|
RESULT_COLUMN_KEY,
|
|
11
10
|
)
|
|
12
11
|
from excelalchemy._primitives.header_models import ExcelHeader
|
|
13
|
-
from excelalchemy._primitives.identity import DataUrlStr, Label, RowIndex, UniqueKey, UniqueLabel, UrlStr
|
|
14
|
-
from excelalchemy._primitives.payloads import DataConverter, ExportRowPayload
|
|
12
|
+
from excelalchemy._primitives.identity import ColumnIndex, DataUrlStr, Label, RowIndex, UniqueKey, UniqueLabel, UrlStr
|
|
13
|
+
from excelalchemy._primitives.payloads import DataConverter, ExportRowPayload
|
|
15
14
|
from excelalchemy.artifacts import ExcelArtifact
|
|
16
15
|
from excelalchemy.codecs.base import SystemReserved
|
|
17
16
|
from excelalchemy.config import ExcelMode, ExporterConfig, ImporterConfig, ImportMode
|
|
18
17
|
from excelalchemy.core.abstract import ABCExcelAlchemy
|
|
19
|
-
from excelalchemy.core.executor import ImportExecutor
|
|
20
18
|
from excelalchemy.core.headers import ExcelHeaderParser, ExcelHeaderValidator
|
|
19
|
+
from excelalchemy.core.import_session import ImportSession, build_import_result_field_meta
|
|
21
20
|
from excelalchemy.core.rendering import ExcelRenderer
|
|
22
|
-
from excelalchemy.core.rows import ImportIssueTracker, RowAggregator
|
|
23
21
|
from excelalchemy.core.schema import ExcelSchemaLayout
|
|
24
22
|
from excelalchemy.core.storage import build_storage_gateway
|
|
25
23
|
from excelalchemy.core.storage_protocol import ExcelStorage
|
|
@@ -30,7 +28,7 @@ from excelalchemy.i18n.messages import MessageKey, use_display_locale
|
|
|
30
28
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
31
29
|
from excelalchemy.i18n.messages import message as msg
|
|
32
30
|
from excelalchemy.metadata import FieldMetaInfo
|
|
33
|
-
from excelalchemy.results import ImportResult
|
|
31
|
+
from excelalchemy.results import ImportResult
|
|
34
32
|
from excelalchemy.util.file import flatten
|
|
35
33
|
|
|
36
34
|
HEADER_HINT_LINE_COUNT = 1
|
|
@@ -48,33 +46,30 @@ REASON_COLUMN.excel_codec = SystemReserved
|
|
|
48
46
|
|
|
49
47
|
class ExcelAlchemy[
|
|
50
48
|
ContextT,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
UpdateModelT: BaseModel,
|
|
55
|
-
ExporterModelT: BaseModel,
|
|
49
|
+
ImportCreateModelT: BaseModel,
|
|
50
|
+
ImportUpdateModelT: BaseModel,
|
|
51
|
+
ExportModelT: BaseModel,
|
|
56
52
|
](
|
|
57
53
|
ABCExcelAlchemy[
|
|
58
54
|
ContextT,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
UpdateModelT,
|
|
63
|
-
ExporterModelT,
|
|
55
|
+
ImportCreateModelT,
|
|
56
|
+
ImportUpdateModelT,
|
|
57
|
+
ExportModelT,
|
|
64
58
|
]
|
|
65
59
|
):
|
|
66
60
|
def __init__(
|
|
67
61
|
self,
|
|
68
|
-
config: ImporterConfig[ContextT,
|
|
62
|
+
config: ImporterConfig[ContextT, ImportCreateModelT, ImportUpdateModelT] | ExporterConfig[ExportModelT],
|
|
69
63
|
):
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
runtime_config = cast(object, config)
|
|
65
|
+
if not isinstance(runtime_config, (ImporterConfig, ExporterConfig)):
|
|
66
|
+
raise ConfigError(msg(MessageKey.EXPORT_MODE_CONFIG_REQUIRED, config_name=ExporterConfig.__name__))
|
|
72
67
|
self.config = config
|
|
73
|
-
self.
|
|
74
|
-
self.
|
|
75
|
-
self.
|
|
68
|
+
self.locale = config.schema_options.locale
|
|
69
|
+
self._context: ContextT | None = getattr(config.behavior, 'context', None)
|
|
70
|
+
self._last_import_session: ImportSession[ContextT, ImportCreateModelT, ImportUpdateModelT] | None = None
|
|
76
71
|
|
|
77
|
-
self.import_result_field_meta = self.
|
|
72
|
+
self.import_result_field_meta = build_import_result_field_meta(locale=self.locale)
|
|
78
73
|
self.import_result_label_to_field_meta = {
|
|
79
74
|
field_meta.unique_label: field_meta for field_meta in self.import_result_field_meta
|
|
80
75
|
}
|
|
@@ -84,27 +79,15 @@ class ExcelAlchemy[
|
|
|
84
79
|
self._renderer = ExcelRenderer()
|
|
85
80
|
self._storage_gateway: ExcelStorage = build_storage_gateway(config)
|
|
86
81
|
self._layout: ExcelSchemaLayout
|
|
87
|
-
self._issue_tracker: ImportIssueTracker | None = None
|
|
88
|
-
self._row_aggregator: RowAggregator | None = None
|
|
89
|
-
self._executor: ImportExecutor[ContextT, ImporterCreateModelT, ImporterUpdateModelT] | None = None
|
|
90
82
|
|
|
91
83
|
self.__init_from_config__()
|
|
92
84
|
|
|
93
85
|
def __init_from_config__(self) -> None:
|
|
94
|
-
self.context = getattr(self.config, 'context', None)
|
|
95
86
|
model = self.__get_importer_model__()
|
|
96
87
|
with use_display_locale(self.locale):
|
|
97
88
|
self._layout = ExcelSchemaLayout.from_model(model)
|
|
98
89
|
self.__sync_layout_state__()
|
|
99
90
|
|
|
100
|
-
self._issue_tracker = ImportIssueTracker(self._layout, self.import_result_field_meta)
|
|
101
|
-
self.cell_errors = self._issue_tracker.cell_errors
|
|
102
|
-
self.row_errors = self._issue_tracker.row_errors
|
|
103
|
-
|
|
104
|
-
if isinstance(self.config, ImporterConfig):
|
|
105
|
-
self._row_aggregator = RowAggregator(self._layout, self.config.import_mode)
|
|
106
|
-
self._executor = ImportExecutor(self.config, self._issue_tracker, lambda: self.context)
|
|
107
|
-
|
|
108
91
|
def __sync_layout_state__(self) -> None:
|
|
109
92
|
self.field_metas = self._layout.field_metas
|
|
110
93
|
self.unique_label_to_field_meta = self._layout.unique_label_to_field_meta
|
|
@@ -113,34 +96,19 @@ class ExcelAlchemy[
|
|
|
113
96
|
self.unique_key_to_field_meta = self._layout.unique_key_to_field_meta
|
|
114
97
|
self.ordered_field_meta = self._layout.ordered_field_meta
|
|
115
98
|
|
|
116
|
-
def
|
|
117
|
-
self.df = WorksheetTable()
|
|
118
|
-
self.header_df = WorksheetTable()
|
|
119
|
-
self.__state_df_has_been_loaded__ = False
|
|
120
|
-
runtime_state = vars(self)
|
|
121
|
-
runtime_state.pop('input_excel_has_merged_header', None)
|
|
122
|
-
runtime_state.pop('input_excel_headers', None)
|
|
123
|
-
|
|
124
|
-
self._issue_tracker = ImportIssueTracker(self._layout, self.import_result_field_meta)
|
|
125
|
-
self.cell_errors = self._issue_tracker.cell_errors
|
|
126
|
-
self.row_errors = self._issue_tracker.row_errors
|
|
127
|
-
|
|
128
|
-
if isinstance(self.config, ImporterConfig):
|
|
129
|
-
self._executor = ImportExecutor(self.config, self._issue_tracker, lambda: self.context)
|
|
130
|
-
|
|
131
|
-
def __get_importer_model__(self) -> type[ImporterCreateModelT] | type[ImporterUpdateModelT] | type[ExporterModelT]:
|
|
99
|
+
def __get_importer_model__(self) -> type[ImportCreateModelT] | type[ImportUpdateModelT] | type[ExportModelT]:
|
|
132
100
|
importer_model = None
|
|
133
101
|
if self.excel_mode == ExcelMode.IMPORT:
|
|
134
102
|
if not isinstance(self.config, ImporterConfig):
|
|
135
103
|
raise ConfigError(msg(MessageKey.IMPORT_MODE_CONFIG_REQUIRED, config_name=ImporterConfig.__name__))
|
|
136
|
-
if self.config.import_mode in (ImportMode.CREATE, ImportMode.CREATE_OR_UPDATE):
|
|
137
|
-
importer_model = self.config.create_importer_model # type: ignore[assignment]
|
|
138
|
-
elif self.config.import_mode == ImportMode.UPDATE:
|
|
139
|
-
importer_model = self.config.update_importer_model # type: ignore[assignment]
|
|
104
|
+
if self.config.behavior.import_mode in (ImportMode.CREATE, ImportMode.CREATE_OR_UPDATE):
|
|
105
|
+
importer_model = self.config.schema_options.create_importer_model # type: ignore[assignment]
|
|
106
|
+
elif self.config.behavior.import_mode == ImportMode.UPDATE:
|
|
107
|
+
importer_model = self.config.schema_options.update_importer_model # type: ignore[assignment]
|
|
140
108
|
elif self.excel_mode == ExcelMode.EXPORT:
|
|
141
109
|
if not isinstance(self.config, ExporterConfig):
|
|
142
110
|
raise ConfigError(msg(MessageKey.EXPORT_MODE_CONFIG_REQUIRED, config_name=ExporterConfig.__name__))
|
|
143
|
-
importer_model = self.config.exporter_model # type: ignore[assignment]
|
|
111
|
+
importer_model = self.config.schema_options.exporter_model # type: ignore[assignment]
|
|
144
112
|
|
|
145
113
|
if importer_model is None:
|
|
146
114
|
raise ConfigError(msg(MessageKey.NO_IMPORTER_OR_EXPORTER_MODEL_CONFIGURED))
|
|
@@ -154,11 +122,13 @@ class ExcelAlchemy[
|
|
|
154
122
|
keys = self._select_output_excel_keys()
|
|
155
123
|
has_merged_header = self.has_merged_header(keys)
|
|
156
124
|
if has_merged_header:
|
|
157
|
-
|
|
125
|
+
worksheet_table = self._export_with_merged_header(sample_data, keys)
|
|
158
126
|
else:
|
|
159
|
-
|
|
127
|
+
worksheet_table = self._export_with_simple_header(sample_data, keys)
|
|
160
128
|
return self._renderer.render_template(
|
|
161
|
-
|
|
129
|
+
worksheet_table,
|
|
130
|
+
self.unique_label_to_field_meta,
|
|
131
|
+
has_merged_header=has_merged_header,
|
|
162
132
|
)
|
|
163
133
|
|
|
164
134
|
def download_template_artifact(
|
|
@@ -174,46 +144,15 @@ class ExcelAlchemy[
|
|
|
174
144
|
if self.excel_mode != ExcelMode.IMPORT:
|
|
175
145
|
raise ConfigError(msg(MessageKey.IMPORT_MODE_ONLY_METHOD))
|
|
176
146
|
|
|
177
|
-
self.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
with use_display_locale(self.locale):
|
|
181
|
-
validate_header = self._validate_header(input_excel_name)
|
|
182
|
-
if not validate_header.is_valid:
|
|
183
|
-
return ImportResult.from_validate_header_result(validate_header)
|
|
184
|
-
|
|
185
|
-
self.df = self.df.iloc[1:]
|
|
186
|
-
self._set_columns(self.df)
|
|
187
|
-
self.df = self.df.reset_index(drop=True)
|
|
188
|
-
|
|
189
|
-
all_success, success_count, fail_count = True, 0, 0
|
|
190
|
-
for table_row_index in range(self.extra_header_count_on_import, len(self.df)):
|
|
191
|
-
row = self.df.row_at(table_row_index)
|
|
192
|
-
aggregate_data = self._aggregate_data(cast(FlatRowPayload, row.to_dict()))
|
|
193
|
-
success = await self._executor.execute(RowIndex(table_row_index), aggregate_data, self.df)
|
|
194
|
-
all_success = all_success and success
|
|
195
|
-
success_count, fail_count = (
|
|
196
|
-
(success_count + 1, fail_count) if success else (success_count, fail_count + 1)
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
url = None
|
|
200
|
-
if not all_success:
|
|
201
|
-
self._add_result_column()
|
|
202
|
-
content_with_prefix = self._render_import_result_excel()
|
|
203
|
-
url = self._upload_file(output_excel_name, content_with_prefix)
|
|
204
|
-
|
|
205
|
-
return ImportResult(
|
|
206
|
-
result=(ValidateResult.DATA_INVALID, ValidateResult.SUCCESS)[int(all_success)],
|
|
207
|
-
url=url,
|
|
208
|
-
success_count=success_count,
|
|
209
|
-
fail_count=fail_count,
|
|
210
|
-
)
|
|
147
|
+
session = self._new_import_session()
|
|
148
|
+
self._last_import_session = session
|
|
149
|
+
return await session.run(input_excel_name, output_excel_name)
|
|
211
150
|
|
|
212
151
|
def export(self, data: list[ExportRowPayload], keys: Sequence[str] | None = None) -> DataUrlStr:
|
|
213
152
|
with use_display_locale(self.locale):
|
|
214
|
-
|
|
153
|
+
worksheet_table, has_merged_header = self._gen_export_df(data, keys)
|
|
215
154
|
return self._renderer.render_data(
|
|
216
|
-
|
|
155
|
+
worksheet_table,
|
|
217
156
|
field_meta_mapping=self.unique_label_to_field_meta,
|
|
218
157
|
has_merged_header=has_merged_header,
|
|
219
158
|
errors={},
|
|
@@ -234,21 +173,57 @@ class ExcelAlchemy[
|
|
|
234
173
|
return self._upload_file(output_name, self.export(data, keys))
|
|
235
174
|
|
|
236
175
|
def add_context(self, context: ContextT) -> None:
|
|
237
|
-
if self.
|
|
176
|
+
if self._context is not None:
|
|
238
177
|
logging.warning('An existing conversion context is being replaced')
|
|
239
|
-
self.
|
|
178
|
+
self._context = context
|
|
179
|
+
if self._last_import_session is not None:
|
|
180
|
+
self._last_import_session.context = context
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def context(self) -> ContextT | None:
|
|
184
|
+
return self._context
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def df(self) -> WorksheetTable:
|
|
188
|
+
"""Backward-compatible alias for worksheet_table."""
|
|
189
|
+
if self._last_import_session is None:
|
|
190
|
+
return WorksheetTable()
|
|
191
|
+
return self._last_import_session.worksheet_table
|
|
240
192
|
|
|
241
|
-
@
|
|
193
|
+
@property
|
|
194
|
+
def worksheet_table(self) -> WorksheetTable:
|
|
195
|
+
return self.df
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def header_df(self) -> WorksheetTable:
|
|
199
|
+
"""Backward-compatible alias for header_table."""
|
|
200
|
+
if self._last_import_session is None:
|
|
201
|
+
return WorksheetTable()
|
|
202
|
+
return self._last_import_session.header_table
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def header_table(self) -> WorksheetTable:
|
|
206
|
+
return self.header_df
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def cell_errors(self) -> dict[RowIndex, dict[ColumnIndex, list[ExcelCellError]]]:
|
|
210
|
+
if self._last_import_session is None:
|
|
211
|
+
return {}
|
|
212
|
+
return self._last_import_session.cell_errors
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def row_errors(self) -> dict[RowIndex, list[ExcelRowError | ExcelCellError]]:
|
|
216
|
+
if self._last_import_session is None:
|
|
217
|
+
return {}
|
|
218
|
+
return self._last_import_session.row_errors
|
|
219
|
+
|
|
220
|
+
@property
|
|
242
221
|
def input_excel_has_merged_header(self) -> bool:
|
|
243
|
-
|
|
244
|
-
raise ConfigError(msg(MessageKey.WORKSHEET_TABLE_NOT_LOADED))
|
|
245
|
-
return self._header_parser.has_merged_header(self.header_df)
|
|
222
|
+
return self._require_last_import_session().input_excel_has_merged_header
|
|
246
223
|
|
|
247
|
-
@
|
|
224
|
+
@property
|
|
248
225
|
def input_excel_headers(self) -> list[ExcelHeader]:
|
|
249
|
-
|
|
250
|
-
raise ConfigError(msg(MessageKey.WORKSHEET_TABLE_NOT_LOADED))
|
|
251
|
-
return self._header_parser.extract(self.header_df)
|
|
226
|
+
return self._require_last_import_session().input_excel_headers
|
|
252
227
|
|
|
253
228
|
@property
|
|
254
229
|
def excel_mode(self) -> ExcelMode:
|
|
@@ -260,26 +235,22 @@ class ExcelAlchemy[
|
|
|
260
235
|
def extra_header_count_on_import(self) -> int:
|
|
261
236
|
if self.excel_mode != ExcelMode.IMPORT:
|
|
262
237
|
raise ConfigError(msg(MessageKey.IMPORT_MODE_ONLY_PROPERTY))
|
|
263
|
-
|
|
264
|
-
for input_excel_label in self.input_excel_headers:
|
|
265
|
-
if input_excel_label.label != input_excel_label.parent_label:
|
|
266
|
-
return 1
|
|
267
|
-
return 0
|
|
238
|
+
return self._require_last_import_session().extra_header_count_on_import
|
|
268
239
|
|
|
269
240
|
@property
|
|
270
|
-
def exporter_model(self) -> type[
|
|
241
|
+
def exporter_model(self) -> type[ExportModelT]:
|
|
271
242
|
if isinstance(self.config, ImporterConfig):
|
|
272
|
-
if self.config.create_importer_model and self.config.update_importer_model:
|
|
243
|
+
if self.config.schema_options.create_importer_model and self.config.schema_options.update_importer_model:
|
|
273
244
|
raise ConfigError(msg(MessageKey.EXPORTER_MODEL_INFERENCE_CONFLICT))
|
|
274
|
-
if self.config.create_importer_model:
|
|
245
|
+
if self.config.schema_options.create_importer_model:
|
|
275
246
|
logging.info('Inferring exporter_model from create_importer_model')
|
|
276
|
-
return cast(type[
|
|
277
|
-
if self.config.update_importer_model:
|
|
247
|
+
return cast(type[ExportModelT], self.config.schema_options.create_importer_model)
|
|
248
|
+
if self.config.schema_options.update_importer_model:
|
|
278
249
|
logging.info('Inferring exporter_model from update_importer_model')
|
|
279
|
-
return cast(type[
|
|
250
|
+
return cast(type[ExportModelT], self.config.schema_options.update_importer_model)
|
|
280
251
|
raise ConfigError(msg(MessageKey.EXPORTER_MODEL_CANNOT_BE_INFERRED))
|
|
281
252
|
|
|
282
|
-
return self.config.exporter_model
|
|
253
|
+
return self.config.schema_options.exporter_model
|
|
283
254
|
|
|
284
255
|
def has_merged_header(self, selected_keys: list[UniqueKey]) -> bool:
|
|
285
256
|
return self._layout.has_merged_header(selected_keys)
|
|
@@ -314,51 +285,18 @@ class ExcelAlchemy[
|
|
|
314
285
|
selected_keys = self._select_output_excel_keys(list(set(input_keys).intersection(set(model_keys))))
|
|
315
286
|
has_merged_header = self.has_merged_header(selected_keys)
|
|
316
287
|
if has_merged_header:
|
|
317
|
-
|
|
288
|
+
worksheet_table = self._export_with_merged_header(data, selected_keys, self.config.behavior.data_converter)
|
|
318
289
|
else:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
assert isinstance(self.config, ImporterConfig)
|
|
326
|
-
self._read_dataframe(input_excel_name)
|
|
327
|
-
return self._header_validator.validate(self.input_excel_headers, self._layout, self.config.import_mode)
|
|
328
|
-
|
|
329
|
-
def _render_import_result_excel(self) -> DataUrlStr:
|
|
330
|
-
return self._renderer.render_data(
|
|
331
|
-
self.df,
|
|
332
|
-
field_meta_mapping=self.import_result_label_to_field_meta | self.unique_label_to_field_meta,
|
|
333
|
-
has_merged_header=self.input_excel_has_merged_header,
|
|
334
|
-
errors=self.cell_errors,
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
def _upload_file(self, output_name: str, content_with_prefix: DataUrlStr) -> UrlStr:
|
|
338
|
-
return self._storage_gateway.upload_excel(output_name, content_with_prefix)
|
|
339
|
-
|
|
340
|
-
def _order_errors(self, errors: list[ExcelRowError | ExcelCellError]) -> Iterable[ExcelCellError | ExcelRowError]:
|
|
341
|
-
return self._layout.order_errors(errors)
|
|
342
|
-
|
|
343
|
-
def _set_columns(self, df: WorksheetTable) -> WorksheetTable:
|
|
344
|
-
return self._header_parser.apply_columns(df, self.input_excel_headers, self.get_output_parent_excel_headers())
|
|
290
|
+
worksheet_table = self._export_with_simple_header(
|
|
291
|
+
data,
|
|
292
|
+
selected_keys,
|
|
293
|
+
self.config.behavior.data_converter,
|
|
294
|
+
)
|
|
295
|
+
return worksheet_table, has_merged_header
|
|
345
296
|
|
|
346
297
|
def _select_output_excel_keys(self, keys: Sequence[str] | None = None) -> list[UniqueKey]:
|
|
347
298
|
return self._layout.select_output_excel_keys(keys)
|
|
348
299
|
|
|
349
|
-
def _read_dataframe(self, input_excel_name: str) -> WorksheetTable:
|
|
350
|
-
assert isinstance(self.config, ImporterConfig)
|
|
351
|
-
if not self.__state_df_has_been_loaded__:
|
|
352
|
-
df = self._storage_gateway.read_excel_table(
|
|
353
|
-
input_excel_name,
|
|
354
|
-
skiprows=HEADER_HINT_LINE_COUNT,
|
|
355
|
-
sheet_name=self.config.sheet_name,
|
|
356
|
-
)
|
|
357
|
-
self.df = df
|
|
358
|
-
self.header_df = df.head(2)
|
|
359
|
-
self.__state_df_has_been_loaded__ = True
|
|
360
|
-
return self.df
|
|
361
|
-
|
|
362
300
|
def _generate_export_df(
|
|
363
301
|
self,
|
|
364
302
|
records: list[ExportRowPayload] | None,
|
|
@@ -396,57 +334,28 @@ class ExcelAlchemy[
|
|
|
396
334
|
) -> WorksheetTable:
|
|
397
335
|
return self._generate_export_df(records, selected_keys, data_converter)
|
|
398
336
|
|
|
399
|
-
def
|
|
400
|
-
assert self.
|
|
401
|
-
|
|
402
|
-
self.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
337
|
+
def _new_import_session(self) -> ImportSession[ContextT, ImportCreateModelT, ImportUpdateModelT]:
|
|
338
|
+
assert isinstance(self.config, ImporterConfig)
|
|
339
|
+
return ImportSession(
|
|
340
|
+
config=self.config,
|
|
341
|
+
layout=self._layout,
|
|
342
|
+
storage_gateway=self._storage_gateway,
|
|
343
|
+
header_parser=self._header_parser,
|
|
344
|
+
header_validator=self._header_validator,
|
|
345
|
+
renderer=self._renderer,
|
|
346
|
+
import_result_field_meta=self.import_result_field_meta,
|
|
347
|
+
import_result_label_to_field_meta=self.import_result_label_to_field_meta,
|
|
348
|
+
locale=self.locale,
|
|
349
|
+
context=self._context,
|
|
406
350
|
)
|
|
407
|
-
return self
|
|
408
|
-
|
|
409
|
-
def _aggregate_data(self, row_data: FlatRowPayload) -> ModelRowPayload:
|
|
410
|
-
assert self._row_aggregator is not None
|
|
411
|
-
return self._row_aggregator.aggregate(row_data)
|
|
412
|
-
|
|
413
|
-
def _register_row_error(
|
|
414
|
-
self,
|
|
415
|
-
row_index: RowIndex,
|
|
416
|
-
error: ExcelRowError | ExcelCellError | list[ExcelRowError | ExcelCellError] | list[ExcelCellError],
|
|
417
|
-
):
|
|
418
|
-
assert self._issue_tracker is not None
|
|
419
|
-
self._issue_tracker.register_row_error(row_index, error)
|
|
420
|
-
|
|
421
|
-
def _register_cell_errors(self, row_index: RowIndex, errors: list[ExcelCellError]):
|
|
422
|
-
assert self._issue_tracker is not None
|
|
423
|
-
self._issue_tracker.register_cell_errors(row_index, errors, self.df)
|
|
424
|
-
return self
|
|
425
|
-
|
|
426
|
-
def _build_import_result_field_meta(self) -> list[FieldMetaInfo]:
|
|
427
|
-
result_column = FieldMetaInfo(label=dmsg(MessageKey.RESULT_COLUMN_LABEL, locale=self.locale))
|
|
428
|
-
result_column.parent_label = result_column.label
|
|
429
|
-
result_column.key = result_column.parent_key = RESULT_COLUMN_KEY
|
|
430
|
-
result_column.excel_codec = SystemReserved
|
|
431
351
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return [result_column, reason_column]
|
|
438
|
-
|
|
439
|
-
def _excel_has_merged_header(self) -> bool:
|
|
440
|
-
return self._header_parser.has_merged_header(self.header_df)
|
|
441
|
-
|
|
442
|
-
def _extract_header(self) -> list[ExcelHeader]:
|
|
443
|
-
return self._header_parser.extract(self.header_df)
|
|
444
|
-
|
|
445
|
-
def _extract_simple_header(self) -> list[ExcelHeader]:
|
|
446
|
-
return self._header_parser.extract_simple(self.header_df)
|
|
352
|
+
def _require_last_import_session(self) -> ImportSession[ContextT, ImportCreateModelT, ImportUpdateModelT]:
|
|
353
|
+
if self._last_import_session is None:
|
|
354
|
+
raise ConfigError(msg(MessageKey.WORKSHEET_TABLE_NOT_LOADED))
|
|
355
|
+
return self._last_import_session
|
|
447
356
|
|
|
448
|
-
def
|
|
449
|
-
return self.
|
|
357
|
+
def _upload_file(self, output_name: str, content_with_prefix: DataUrlStr) -> UrlStr:
|
|
358
|
+
return self._storage_gateway.upload_excel(output_name, content_with_prefix)
|
|
450
359
|
|
|
451
360
|
def __setattr__(self, key: str, value: object):
|
|
452
361
|
if key == 'config' and hasattr(self, 'config'):
|