ExcelAlchemy 2.2.7__tar.gz → 2.2.8__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.2.7 → excelalchemy-2.2.8}/PKG-INFO +3 -3
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/README-pypi.md +2 -2
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/__init__.py +1 -1
- excelalchemy-2.2.8/src/excelalchemy/_primitives/diagnostics.py +50 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/alchemy.py +11 -8
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/metadata.py +7 -12
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/results.py +122 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/LICENSE +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/pyproject.toml +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/base.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/boolean.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/date.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/date_range.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/multi_checkbox.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/number.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/number_range.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/organization.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/phone_number.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/radio.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/staff.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/tree.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/codecs/url.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/headers.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/import_session.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/rows.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/schema.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/core/writer.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/exceptions.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/helper/pydantic.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/i18n/messages.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/util/converter.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/util/convertor.py +0 -0
- {excelalchemy-2.2.7 → excelalchemy-2.2.8}/src/excelalchemy/util/file.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ExcelAlchemy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.8
|
|
4
4
|
Summary: Schema-driven Python library for typed Excel import/export workflows with Pydantic and locale-aware workbooks.
|
|
5
5
|
Keywords: excel,openpyxl,pydantic,minio,schema
|
|
6
6
|
Author: Ray
|
|
@@ -49,9 +49,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
|
|
|
49
49
|
- render workbook-facing output in `zh-CN` or `en`
|
|
50
50
|
- keep storage pluggable through `ExcelStorage`
|
|
51
51
|
|
|
52
|
-
The current stable release is `2.2.
|
|
52
|
+
The current stable release is `2.2.8`, which continues the 2.x line with a clearer integration roadmap, stronger import-failure payload smoke verification, and more direct install-time validation of the FastAPI reference app.
|
|
53
53
|
|
|
54
|
-
[GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
54
|
+
[GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Integration Roadmap](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/integration-roadmap.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
55
55
|
|
|
56
56
|
## Screenshots
|
|
57
57
|
|
|
@@ -10,9 +10,9 @@ ExcelAlchemy turns Pydantic models into typed workbook contracts:
|
|
|
10
10
|
- render workbook-facing output in `zh-CN` or `en`
|
|
11
11
|
- keep storage pluggable through `ExcelStorage`
|
|
12
12
|
|
|
13
|
-
The current stable release is `2.2.
|
|
13
|
+
The current stable release is `2.2.8`, which continues the 2.x line with a clearer integration roadmap, stronger import-failure payload smoke verification, and more direct install-time validation of the FastAPI reference app.
|
|
14
14
|
|
|
15
|
-
[GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
15
|
+
[GitHub Repository](https://github.com/RayCarterLab/ExcelAlchemy) · [Full README](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/README.md) · [Getting Started](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/getting-started.md) · [Integration Roadmap](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/integration-roadmap.md) · [Result Objects](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/result-objects.md) · [API Response Cookbook](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/api-response-cookbook.md) · [Examples Showcase](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/examples-showcase.md) · [Architecture](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/docs/architecture.md) · [Migration Notes](https://github.com/RayCarterLab/ExcelAlchemy/blob/main/MIGRATIONS.md)
|
|
16
16
|
|
|
17
17
|
## Screenshots
|
|
18
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""A Python Library for Reading and Writing Excel Files"""
|
|
2
2
|
|
|
3
|
-
__version__ = '2.2.
|
|
3
|
+
__version__ = '2.2.8'
|
|
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 (
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Named diagnostic loggers and helpers for developer-facing runtime output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
RUNTIME_LOGGER_NAME = 'excelalchemy.runtime'
|
|
8
|
+
METADATA_LOGGER_NAME = 'excelalchemy.metadata'
|
|
9
|
+
|
|
10
|
+
runtime_logger = logging.getLogger(RUNTIME_LOGGER_NAME)
|
|
11
|
+
metadata_logger = logging.getLogger(METADATA_LOGGER_NAME)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def log_runtime_context_replacement() -> None:
|
|
15
|
+
runtime_logger.warning(
|
|
16
|
+
'Replacing an existing conversion context; subsequent imports will use the new runtime context.'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def log_runtime_exporter_inference(*, source: str) -> None:
|
|
21
|
+
runtime_logger.info('Inferring exporter_model from %s.', source)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def log_runtime_export_requested_in_import_mode() -> None:
|
|
25
|
+
runtime_logger.info('Export requested while configured in import mode; inferring exporter_model and continuing.')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def log_runtime_ignoring_unrecognized_export_keys(*, unrecognized: set[str], model_keys: list[str]) -> None:
|
|
29
|
+
runtime_logger.warning(
|
|
30
|
+
'Ignoring export keys that are not present in the exporter model. Ignored keys: %s. Exporter model keys: %s.',
|
|
31
|
+
sorted(unrecognized),
|
|
32
|
+
model_keys,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def log_metadata_large_option_set(*, field_label: str, option_count: int) -> None:
|
|
37
|
+
metadata_logger.warning(
|
|
38
|
+
'Field "%s" defines %s options. Options are intended for bounded vocabularies, so review this field if it '
|
|
39
|
+
'represents a large dataset.',
|
|
40
|
+
field_label,
|
|
41
|
+
option_count,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def log_metadata_missing_option_id(*, option_id: str, field_label: str) -> None:
|
|
46
|
+
metadata_logger.warning(
|
|
47
|
+
'Could not resolve option id %s for field "%s"; returning the original workbook value.',
|
|
48
|
+
option_id,
|
|
49
|
+
field_label,
|
|
50
|
+
)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from collections.abc import Sequence
|
|
3
2
|
from typing import cast
|
|
4
3
|
|
|
@@ -8,6 +7,12 @@ from excelalchemy._primitives.constants import (
|
|
|
8
7
|
REASON_COLUMN_KEY,
|
|
9
8
|
RESULT_COLUMN_KEY,
|
|
10
9
|
)
|
|
10
|
+
from excelalchemy._primitives.diagnostics import (
|
|
11
|
+
log_runtime_context_replacement,
|
|
12
|
+
log_runtime_export_requested_in_import_mode,
|
|
13
|
+
log_runtime_exporter_inference,
|
|
14
|
+
log_runtime_ignoring_unrecognized_export_keys,
|
|
15
|
+
)
|
|
11
16
|
from excelalchemy._primitives.header_models import ExcelHeader
|
|
12
17
|
from excelalchemy._primitives.identity import DataUrlStr, Label, UniqueKey, UniqueLabel, UrlStr
|
|
13
18
|
from excelalchemy._primitives.payloads import DataConverter, ExportRowPayload
|
|
@@ -174,7 +179,7 @@ class ExcelAlchemy[
|
|
|
174
179
|
|
|
175
180
|
def add_context(self, context: ContextT) -> None:
|
|
176
181
|
if self._context is not None:
|
|
177
|
-
|
|
182
|
+
log_runtime_context_replacement()
|
|
178
183
|
self._context = context
|
|
179
184
|
if self._last_import_session is not None:
|
|
180
185
|
self._last_import_session.context = context
|
|
@@ -263,10 +268,10 @@ class ExcelAlchemy[
|
|
|
263
268
|
if self.config.schema_options.create_importer_model and self.config.schema_options.update_importer_model:
|
|
264
269
|
raise ConfigError(msg(MessageKey.EXPORTER_MODEL_INFERENCE_CONFLICT))
|
|
265
270
|
if self.config.schema_options.create_importer_model:
|
|
266
|
-
|
|
271
|
+
log_runtime_exporter_inference(source='create_importer_model')
|
|
267
272
|
return cast(type[ExportModelT], self.config.schema_options.create_importer_model)
|
|
268
273
|
if self.config.schema_options.update_importer_model:
|
|
269
|
-
|
|
274
|
+
log_runtime_exporter_inference(source='update_importer_model')
|
|
270
275
|
return cast(type[ExportModelT], self.config.schema_options.update_importer_model)
|
|
271
276
|
raise ConfigError(msg(MessageKey.EXPORTER_MODEL_CANNOT_BE_INFERRED))
|
|
272
277
|
|
|
@@ -285,7 +290,7 @@ class ExcelAlchemy[
|
|
|
285
290
|
self, data: list[ExportRowPayload], keys: Sequence[str] | None = None
|
|
286
291
|
) -> tuple[WorksheetTable, bool]:
|
|
287
292
|
if self.excel_mode == ExcelMode.IMPORT:
|
|
288
|
-
|
|
293
|
+
log_runtime_export_requested_in_import_mode()
|
|
289
294
|
|
|
290
295
|
input_keys = (
|
|
291
296
|
list(keys)
|
|
@@ -298,9 +303,7 @@ class ExcelAlchemy[
|
|
|
298
303
|
)
|
|
299
304
|
model_keys = get_model_field_names(self.exporter_model)
|
|
300
305
|
if unrecognized := (set(input_keys) - set(model_keys)):
|
|
301
|
-
|
|
302
|
-
'Ignoring keys not present in the exporter model: %s (model keys: %s)', unrecognized, model_keys
|
|
303
|
-
)
|
|
306
|
+
log_runtime_ignoring_unrecognized_export_keys(unrecognized=unrecognized, model_keys=model_keys)
|
|
304
307
|
|
|
305
308
|
selected_keys = self._select_output_excel_keys(list(set(input_keys).intersection(set(model_keys))))
|
|
306
309
|
has_merged_header = self.has_merged_header(selected_keys)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import datetime
|
|
5
|
-
import logging
|
|
6
5
|
from collections.abc import Callable, Mapping, Set
|
|
7
6
|
from dataclasses import dataclass, field, replace
|
|
8
7
|
from functools import cached_property
|
|
@@ -25,6 +24,10 @@ from excelalchemy._primitives.constants import (
|
|
|
25
24
|
IntStr,
|
|
26
25
|
Option,
|
|
27
26
|
)
|
|
27
|
+
from excelalchemy._primitives.diagnostics import (
|
|
28
|
+
log_metadata_large_option_set,
|
|
29
|
+
log_metadata_missing_option_id,
|
|
30
|
+
)
|
|
28
31
|
from excelalchemy._primitives.identity import Key, Label, OptionId, UniqueKey, UniqueLabel
|
|
29
32
|
from excelalchemy.codecs.base import ExcelFieldCodec, UndefinedFieldCodec
|
|
30
33
|
from excelalchemy.exceptions import ConfigError, ProgrammaticError
|
|
@@ -188,22 +191,14 @@ class WorkbookPresentationMeta:
|
|
|
188
191
|
if self.options is None:
|
|
189
192
|
return {}
|
|
190
193
|
if len(self.options) > MAX_OPTIONS_COUNT:
|
|
191
|
-
|
|
192
|
-
'Field "%s" defines %s options; please confirm that this is intentional because options are not meant for large datasets',
|
|
193
|
-
field_label,
|
|
194
|
-
len(self.options),
|
|
195
|
-
)
|
|
194
|
+
log_metadata_large_option_set(field_label=str(field_label), option_count=len(self.options))
|
|
196
195
|
return {option.id: option for option in self.options}
|
|
197
196
|
|
|
198
197
|
def options_name_map(self, *, field_label: Label) -> dict[str, Option]:
|
|
199
198
|
if self.options is None:
|
|
200
199
|
return {}
|
|
201
200
|
if len(self.options) > MAX_OPTIONS_COUNT:
|
|
202
|
-
|
|
203
|
-
'Field "%s" defines %s options; please confirm that this is intentional because options are not meant for large datasets',
|
|
204
|
-
field_label,
|
|
205
|
-
len(self.options),
|
|
206
|
-
)
|
|
201
|
+
log_metadata_large_option_set(field_label=str(field_label), option_count=len(self.options))
|
|
207
202
|
return {option.name: option for option in self.options}
|
|
208
203
|
|
|
209
204
|
def exchange_option_ids_to_names(
|
|
@@ -220,7 +215,7 @@ class WorkbookPresentationMeta:
|
|
|
220
215
|
try:
|
|
221
216
|
option_names.append(option_id_map[normalized_id].name)
|
|
222
217
|
except KeyError:
|
|
223
|
-
|
|
218
|
+
log_metadata_missing_option_id(option_id=str(normalized_id), field_label=str(field_label))
|
|
224
219
|
option_names.append(normalized_id)
|
|
225
220
|
|
|
226
221
|
return option_names
|
|
@@ -152,6 +152,30 @@ class CellErrorMap(dict[RowIndex, dict[ColumnIndex, list[ExcelCellError]]]):
|
|
|
152
152
|
def messages_at(self, row_index: RowIndex | int, column_index: ColumnIndex | int) -> tuple[str, ...]:
|
|
153
153
|
return tuple(str(error) for error in self.at(row_index, column_index))
|
|
154
154
|
|
|
155
|
+
def field_labels(self) -> tuple[str, ...]:
|
|
156
|
+
return tuple(sorted({str(error.label) for error in self.flatten()}))
|
|
157
|
+
|
|
158
|
+
def parent_labels(self) -> tuple[str, ...]:
|
|
159
|
+
return tuple(sorted({str(error.parent_label) for error in self.flatten() if error.parent_label is not None}))
|
|
160
|
+
|
|
161
|
+
def unique_labels(self) -> tuple[str, ...]:
|
|
162
|
+
return tuple(sorted({str(error.unique_label) for error in self.flatten()}))
|
|
163
|
+
|
|
164
|
+
def codes(self) -> tuple[str, ...]:
|
|
165
|
+
return tuple(sorted({error.code for error in self.flatten()}))
|
|
166
|
+
|
|
167
|
+
def row_indices(self) -> tuple[RowIndex, ...]:
|
|
168
|
+
return tuple(sorted(self.keys()))
|
|
169
|
+
|
|
170
|
+
def row_numbers_for_humans(self) -> tuple[int, ...]:
|
|
171
|
+
return tuple(_row_number_for_humans(row_index) for row_index in self.row_indices())
|
|
172
|
+
|
|
173
|
+
def column_indices(self) -> tuple[ColumnIndex, ...]:
|
|
174
|
+
return tuple(sorted({column_index for row in self.values() for column_index in row}))
|
|
175
|
+
|
|
176
|
+
def column_numbers_for_humans(self) -> tuple[int, ...]:
|
|
177
|
+
return tuple(_column_number_for_humans(column_index) for column_index in self.column_indices())
|
|
178
|
+
|
|
155
179
|
def flatten(self) -> tuple[ExcelCellError, ...]:
|
|
156
180
|
return tuple(error for row in self.values() for errors in row.values() for error in errors)
|
|
157
181
|
|
|
@@ -223,6 +247,32 @@ class CellErrorMap(dict[RowIndex, dict[ColumnIndex, list[ExcelCellError]]]):
|
|
|
223
247
|
)
|
|
224
248
|
return tuple(sorted(summaries, key=lambda summary: summary.code))
|
|
225
249
|
|
|
250
|
+
def grouped_messages_by_field(self) -> dict[str, tuple[str, ...]]:
|
|
251
|
+
return {
|
|
252
|
+
summary.unique_label: tuple(
|
|
253
|
+
record.error.display_message
|
|
254
|
+
for record in self.records()
|
|
255
|
+
if str(record.error.unique_label) == summary.unique_label
|
|
256
|
+
)
|
|
257
|
+
for summary in self.summary_by_field()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def grouped_messages_by_row(self) -> dict[int, tuple[str, ...]]:
|
|
261
|
+
return {
|
|
262
|
+
int(summary.row_index): tuple(
|
|
263
|
+
record.error.display_message for record in self.records() if record.row_index == summary.row_index
|
|
264
|
+
)
|
|
265
|
+
for summary in self.summary_by_row()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
def grouped_messages_by_code(self) -> dict[str, tuple[str, ...]]:
|
|
269
|
+
return {
|
|
270
|
+
summary.code: tuple(
|
|
271
|
+
record.error.display_message for record in self.records() if record.error.code == summary.code
|
|
272
|
+
)
|
|
273
|
+
for summary in self.summary_by_code()
|
|
274
|
+
}
|
|
275
|
+
|
|
226
276
|
def to_dict(self) -> dict[int, dict[int, list[dict[str, object]]]]:
|
|
227
277
|
return {
|
|
228
278
|
int(row_index): {
|
|
@@ -236,6 +286,23 @@ class CellErrorMap(dict[RowIndex, dict[ColumnIndex, list[ExcelCellError]]]):
|
|
|
236
286
|
'error_count': self.error_count,
|
|
237
287
|
'items': [record.to_dict() for record in self.records()],
|
|
238
288
|
'by_row': self.to_dict(),
|
|
289
|
+
'facets': {
|
|
290
|
+
'field_labels': list(self.field_labels()),
|
|
291
|
+
'parent_labels': list(self.parent_labels()),
|
|
292
|
+
'unique_labels': list(self.unique_labels()),
|
|
293
|
+
'codes': list(self.codes()),
|
|
294
|
+
'row_numbers_for_humans': list(self.row_numbers_for_humans()),
|
|
295
|
+
'column_numbers_for_humans': list(self.column_numbers_for_humans()),
|
|
296
|
+
},
|
|
297
|
+
'grouped': {
|
|
298
|
+
'messages_by_field': {
|
|
299
|
+
key: list(messages) for key, messages in self.grouped_messages_by_field().items()
|
|
300
|
+
},
|
|
301
|
+
'messages_by_row': {
|
|
302
|
+
str(row_index): list(messages) for row_index, messages in self.grouped_messages_by_row().items()
|
|
303
|
+
},
|
|
304
|
+
'messages_by_code': {key: list(messages) for key, messages in self.grouped_messages_by_code().items()},
|
|
305
|
+
},
|
|
239
306
|
'summary': {
|
|
240
307
|
'by_field': [summary.to_dict() for summary in self.summary_by_field()],
|
|
241
308
|
'by_row': [summary.to_dict() for summary in self.summary_by_row()],
|
|
@@ -267,6 +334,32 @@ class RowIssueMap(dict[RowIndex, list[RowIssue]]):
|
|
|
267
334
|
def messages_for_row(self, row_index: RowIndex | int) -> tuple[str, ...]:
|
|
268
335
|
return tuple(str(error) for error in self.at(row_index))
|
|
269
336
|
|
|
337
|
+
def field_labels(self) -> tuple[str, ...]:
|
|
338
|
+
return tuple(sorted({str(error.label) for error in self.flatten() if isinstance(error, ExcelCellError)}))
|
|
339
|
+
|
|
340
|
+
def parent_labels(self) -> tuple[str, ...]:
|
|
341
|
+
return tuple(
|
|
342
|
+
sorted(
|
|
343
|
+
{
|
|
344
|
+
str(error.parent_label)
|
|
345
|
+
for error in self.flatten()
|
|
346
|
+
if isinstance(error, ExcelCellError) and error.parent_label is not None
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def unique_labels(self) -> tuple[str, ...]:
|
|
352
|
+
return tuple(sorted({str(error.unique_label) for error in self.flatten() if isinstance(error, ExcelCellError)}))
|
|
353
|
+
|
|
354
|
+
def codes(self) -> tuple[str, ...]:
|
|
355
|
+
return tuple(sorted({error.code for error in self.flatten()}))
|
|
356
|
+
|
|
357
|
+
def row_indices(self) -> tuple[RowIndex, ...]:
|
|
358
|
+
return tuple(sorted(self.keys()))
|
|
359
|
+
|
|
360
|
+
def row_numbers_for_humans(self) -> tuple[int, ...]:
|
|
361
|
+
return tuple(_row_number_for_humans(row_index) for row_index in self.row_indices())
|
|
362
|
+
|
|
270
363
|
def numbered_messages_for_row(self, row_index: RowIndex | int) -> tuple[str, ...]:
|
|
271
364
|
return self.numbered_messages(self.at(row_index))
|
|
272
365
|
|
|
@@ -318,6 +411,22 @@ class RowIssueMap(dict[RowIndex, list[RowIssue]]):
|
|
|
318
411
|
)
|
|
319
412
|
return tuple(sorted(summaries, key=lambda summary: summary.code))
|
|
320
413
|
|
|
414
|
+
def grouped_messages_by_row(self) -> dict[int, tuple[str, ...]]:
|
|
415
|
+
return {
|
|
416
|
+
int(summary.row_index): tuple(
|
|
417
|
+
record.error.display_message for record in self.records() if record.row_index == summary.row_index
|
|
418
|
+
)
|
|
419
|
+
for summary in self.summary_by_row()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
def grouped_messages_by_code(self) -> dict[str, tuple[str, ...]]:
|
|
423
|
+
return {
|
|
424
|
+
summary.code: tuple(
|
|
425
|
+
record.error.display_message for record in self.records() if record.error.code == summary.code
|
|
426
|
+
)
|
|
427
|
+
for summary in self.summary_by_code()
|
|
428
|
+
}
|
|
429
|
+
|
|
321
430
|
def to_dict(self) -> dict[int, list[dict[str, object]]]:
|
|
322
431
|
return {int(row_index): [error.to_dict() for error in errors] for row_index, errors in self.items()}
|
|
323
432
|
|
|
@@ -326,6 +435,19 @@ class RowIssueMap(dict[RowIndex, list[RowIssue]]):
|
|
|
326
435
|
'error_count': self.error_count,
|
|
327
436
|
'items': [record.to_dict() for record in self.records()],
|
|
328
437
|
'by_row': self.to_dict(),
|
|
438
|
+
'facets': {
|
|
439
|
+
'field_labels': list(self.field_labels()),
|
|
440
|
+
'parent_labels': list(self.parent_labels()),
|
|
441
|
+
'unique_labels': list(self.unique_labels()),
|
|
442
|
+
'codes': list(self.codes()),
|
|
443
|
+
'row_numbers_for_humans': list(self.row_numbers_for_humans()),
|
|
444
|
+
},
|
|
445
|
+
'grouped': {
|
|
446
|
+
'messages_by_row': {
|
|
447
|
+
str(row_index): list(messages) for row_index, messages in self.grouped_messages_by_row().items()
|
|
448
|
+
},
|
|
449
|
+
'messages_by_code': {key: list(messages) for key, messages in self.grouped_messages_by_code().items()},
|
|
450
|
+
},
|
|
329
451
|
'summary': {
|
|
330
452
|
'by_row': [summary.to_dict() for summary in self.summary_by_row()],
|
|
331
453
|
'by_code': [summary.to_dict() for summary in self.summary_by_code()],
|
|
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
|
|
File without changes
|