ExcelAlchemy 2.2.6__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.6 → excelalchemy-2.2.8}/PKG-INFO +3 -3
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/README-pypi.md +2 -2
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/__init__.py +7 -1
- excelalchemy-2.2.8/src/excelalchemy/_primitives/diagnostics.py +50 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/base.py +46 -6
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/boolean.py +15 -8
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/date.py +2 -2
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/date_range.py +7 -3
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/multi_checkbox.py +7 -7
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/number.py +2 -2
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/organization.py +8 -3
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/radio.py +10 -14
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/staff.py +8 -8
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/tree.py +6 -4
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/alchemy.py +11 -8
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/exceptions.py +17 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/metadata.py +7 -12
- excelalchemy-2.2.8/src/excelalchemy/results.py +579 -0
- excelalchemy-2.2.6/src/excelalchemy/results.py +0 -231
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/LICENSE +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/pyproject.toml +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/constants.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/identity.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/money.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/number_range.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/phone_number.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/string.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/codecs/url.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/headers.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/import_session.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/rows.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/schema.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/storage.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/core/writer.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/helper/pydantic.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/i18n/messages.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/util/converter.py +0 -0
- {excelalchemy-2.2.6 → excelalchemy-2.2.8}/src/excelalchemy/util/convertor.py +0 -0
- {excelalchemy-2.2.6 → 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) · [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) · [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 (
|
|
@@ -51,9 +51,12 @@ from excelalchemy.metadata import ExcelMeta, FieldMeta, PatchFieldMeta
|
|
|
51
51
|
from excelalchemy.results import (
|
|
52
52
|
CellErrorMap,
|
|
53
53
|
CellIssueRecord,
|
|
54
|
+
CodeIssueSummary,
|
|
55
|
+
FieldIssueSummary,
|
|
54
56
|
ImportResult,
|
|
55
57
|
RowIssueMap,
|
|
56
58
|
RowIssueRecord,
|
|
59
|
+
RowIssueSummary,
|
|
57
60
|
ValidateHeaderResult,
|
|
58
61
|
ValidateResult,
|
|
59
62
|
ValidateRowResult,
|
|
@@ -66,6 +69,7 @@ __all__ = [
|
|
|
66
69
|
'BooleanCodec',
|
|
67
70
|
'CellErrorMap',
|
|
68
71
|
'CellIssueRecord',
|
|
72
|
+
'CodeIssueSummary',
|
|
69
73
|
'ColumnIndex',
|
|
70
74
|
'CompositeExcelFieldCodec',
|
|
71
75
|
'ConfigError',
|
|
@@ -87,6 +91,7 @@ __all__ = [
|
|
|
87
91
|
'ExcelRowError',
|
|
88
92
|
'ExcelStorage',
|
|
89
93
|
'ExporterConfig',
|
|
94
|
+
'FieldIssueSummary',
|
|
90
95
|
'FieldMeta',
|
|
91
96
|
'ImportMode',
|
|
92
97
|
'ImportResult',
|
|
@@ -117,6 +122,7 @@ __all__ = [
|
|
|
117
122
|
'RowIndex',
|
|
118
123
|
'RowIssueMap',
|
|
119
124
|
'RowIssueRecord',
|
|
125
|
+
'RowIssueSummary',
|
|
120
126
|
'SingleChoiceCodec',
|
|
121
127
|
'SingleOrganization',
|
|
122
128
|
'SingleOrganizationCodec',
|
|
@@ -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
|
+
)
|
|
@@ -19,6 +19,9 @@ type WorkbookInputValue = Any
|
|
|
19
19
|
type WorkbookDisplayValue = Any
|
|
20
20
|
type NormalizedImportValue = Any
|
|
21
21
|
|
|
22
|
+
CODEC_LOGGER_NAME = 'excelalchemy.codecs'
|
|
23
|
+
codec_logger = logging.getLogger(CODEC_LOGGER_NAME)
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
def _summarize_exception(exc: Exception) -> str:
|
|
24
27
|
details: list[str] = []
|
|
@@ -43,20 +46,29 @@ def _summarize_exception(exc: Exception) -> str:
|
|
|
43
46
|
return exc.__class__.__name__
|
|
44
47
|
|
|
45
48
|
|
|
49
|
+
def _fallback_reason(*, exc: Exception | None = None, reason: str | None = None) -> str:
|
|
50
|
+
if reason:
|
|
51
|
+
return reason
|
|
52
|
+
if exc is not None:
|
|
53
|
+
return _summarize_exception(exc)
|
|
54
|
+
return 'No additional details'
|
|
55
|
+
|
|
56
|
+
|
|
46
57
|
def log_codec_parse_fallback(
|
|
47
58
|
codec_name: str,
|
|
48
59
|
value: object,
|
|
49
60
|
*,
|
|
50
61
|
field_label: str | None = None,
|
|
51
|
-
exc: Exception,
|
|
62
|
+
exc: Exception | None = None,
|
|
63
|
+
reason: str | None = None,
|
|
52
64
|
) -> None:
|
|
53
65
|
field_context = f' for field "{field_label}"' if field_label else ''
|
|
54
|
-
|
|
66
|
+
codec_logger.warning(
|
|
55
67
|
'Codec %s could not parse workbook input%s; keeping the original value %r. Reason: %s',
|
|
56
68
|
codec_name,
|
|
57
69
|
field_context,
|
|
58
70
|
value,
|
|
59
|
-
|
|
71
|
+
_fallback_reason(exc=exc, reason=reason),
|
|
60
72
|
)
|
|
61
73
|
|
|
62
74
|
|
|
@@ -65,15 +77,43 @@ def log_codec_render_fallback(
|
|
|
65
77
|
value: object,
|
|
66
78
|
*,
|
|
67
79
|
field_label: str | None = None,
|
|
68
|
-
exc: Exception,
|
|
80
|
+
exc: Exception | None = None,
|
|
81
|
+
reason: str | None = None,
|
|
69
82
|
) -> None:
|
|
70
83
|
field_context = f' for field "{field_label}"' if field_label else ''
|
|
71
|
-
|
|
84
|
+
codec_logger.warning(
|
|
72
85
|
'Codec %s could not format workbook value%s; returning %r as-is. Reason: %s',
|
|
73
86
|
codec_name,
|
|
74
87
|
field_context,
|
|
75
88
|
value,
|
|
76
|
-
|
|
89
|
+
_fallback_reason(exc=exc, reason=reason),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def log_codec_option_resolution_fallback(
|
|
94
|
+
codec_name: str,
|
|
95
|
+
value: object,
|
|
96
|
+
*,
|
|
97
|
+
field_label: str | None = None,
|
|
98
|
+
exc: Exception | None = None,
|
|
99
|
+
reason: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
102
|
+
codec_logger.warning(
|
|
103
|
+
'Codec %s could not resolve a configured option%s; returning %r as-is. Reason: %s',
|
|
104
|
+
codec_name,
|
|
105
|
+
field_context,
|
|
106
|
+
value,
|
|
107
|
+
_fallback_reason(exc=exc, reason=reason),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def log_codec_missing_options(codec_name: str, *, field_label: str | None = None) -> None:
|
|
112
|
+
field_context = f' for field "{field_label}"' if field_label else ''
|
|
113
|
+
codec_logger.warning(
|
|
114
|
+
'Codec %s is missing configured options%s; workbook comments and validation may be incomplete.',
|
|
115
|
+
codec_name,
|
|
116
|
+
field_context,
|
|
77
117
|
)
|
|
78
118
|
|
|
79
119
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
1
|
from excelalchemy.codecs import excel_choice_codec
|
|
4
|
-
from excelalchemy.codecs.base import
|
|
2
|
+
from excelalchemy.codecs.base import (
|
|
3
|
+
ExcelFieldCodec,
|
|
4
|
+
WorkbookDisplayValue,
|
|
5
|
+
WorkbookInputValue,
|
|
6
|
+
log_codec_render_fallback,
|
|
7
|
+
)
|
|
5
8
|
from excelalchemy.i18n.messages import MessageKey
|
|
6
9
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
7
10
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -63,15 +66,19 @@ class Boolean(ExcelFieldCodec):
|
|
|
63
66
|
if value in cls._false_values():
|
|
64
67
|
return cls._false_display()
|
|
65
68
|
if value not in cls._true_values() | cls._false_values():
|
|
66
|
-
|
|
69
|
+
log_codec_render_fallback(
|
|
70
|
+
cls.__name__,
|
|
71
|
+
value,
|
|
72
|
+
field_label=declared.label,
|
|
73
|
+
reason=f'Expected {cls._true_display()!r} or {cls._false_display()!r}',
|
|
74
|
+
)
|
|
67
75
|
return value
|
|
68
76
|
else:
|
|
69
|
-
|
|
70
|
-
'Type %s could not deserialize %s for field %s; returning the default value %s',
|
|
77
|
+
log_codec_render_fallback(
|
|
71
78
|
cls.__name__,
|
|
72
79
|
value,
|
|
73
|
-
declared.label,
|
|
74
|
-
cls._false_display(),
|
|
80
|
+
field_label=declared.label,
|
|
81
|
+
reason=f'Expected a boolean or one of {cls._true_display()!r}/{cls._false_display()!r}',
|
|
75
82
|
)
|
|
76
83
|
|
|
77
84
|
return cls._true_display() if str(value) in cls._true_values() else cls._false_display()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from datetime import datetime
|
|
3
2
|
from typing import cast
|
|
4
3
|
|
|
@@ -11,6 +10,7 @@ from excelalchemy.codecs.base import (
|
|
|
11
10
|
NormalizedImportValue,
|
|
12
11
|
WorkbookDisplayValue,
|
|
13
12
|
WorkbookInputValue,
|
|
13
|
+
codec_logger,
|
|
14
14
|
log_codec_parse_fallback,
|
|
15
15
|
)
|
|
16
16
|
from excelalchemy.exceptions import ConfigError
|
|
@@ -53,7 +53,7 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
53
53
|
declared = field_meta.declared
|
|
54
54
|
presentation = field_meta.presentation
|
|
55
55
|
if isinstance(value, DateTime):
|
|
56
|
-
|
|
56
|
+
codec_logger.info(
|
|
57
57
|
'Codec %s received a parsed datetime for %s; returning it unchanged: %s',
|
|
58
58
|
cls.__name__,
|
|
59
59
|
declared.label,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from collections.abc import Mapping
|
|
3
2
|
from datetime import datetime
|
|
4
3
|
from typing import cast
|
|
@@ -9,7 +8,7 @@ from pydantic import BaseModel
|
|
|
9
8
|
|
|
10
9
|
from excelalchemy._primitives.constants import DATE_FORMAT_TO_PYTHON_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
|
|
11
10
|
from excelalchemy._primitives.identity import Key
|
|
12
|
-
from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback
|
|
11
|
+
from excelalchemy.codecs.base import CompositeExcelFieldCodec, log_codec_parse_fallback, log_codec_render_fallback
|
|
13
12
|
from excelalchemy.exceptions import ConfigError
|
|
14
13
|
from excelalchemy.i18n.messages import MessageKey
|
|
15
14
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -149,7 +148,12 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
149
148
|
if mapping is not None:
|
|
150
149
|
return cls.__deserialize__dict(py_date_format, mapping)
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
log_codec_render_fallback(
|
|
152
|
+
cls.__name__,
|
|
153
|
+
value,
|
|
154
|
+
field_label=field_meta.declared.label,
|
|
155
|
+
reason='The workbook value is not a string, datetime, or start/end mapping',
|
|
156
|
+
)
|
|
153
157
|
return str(value)
|
|
154
158
|
|
|
155
159
|
@classmethod
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from typing import cast
|
|
3
2
|
|
|
4
3
|
from excelalchemy._primitives.constants import MULTI_CHECKBOX_SEPARATOR
|
|
5
4
|
from excelalchemy._primitives.identity import OptionId
|
|
6
|
-
from excelalchemy.codecs.base import ExcelFieldCodec
|
|
5
|
+
from excelalchemy.codecs.base import ExcelFieldCodec, log_codec_missing_options, log_codec_parse_fallback
|
|
7
6
|
from excelalchemy.exceptions import ProgrammaticError
|
|
8
7
|
from excelalchemy.i18n.messages import MessageKey
|
|
9
8
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -73,8 +72,11 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
73
72
|
if isinstance(value, str):
|
|
74
73
|
return [item.strip() for item in value.split(MULTI_CHECKBOX_SEPARATOR)]
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
log_codec_parse_fallback(
|
|
76
|
+
cls.__name__,
|
|
77
|
+
value,
|
|
78
|
+
field_label=field_meta.declared.label,
|
|
79
|
+
reason='Expected a delimited string or a list of selected values',
|
|
78
80
|
)
|
|
79
81
|
return value
|
|
80
82
|
|
|
@@ -92,9 +94,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
92
94
|
raise ProgrammaticError(msg(MessageKey.OPTIONS_CANNOT_BE_NONE_FOR_VALUE_TYPE, value_type=cls.__name__))
|
|
93
95
|
|
|
94
96
|
if not presentation.options: # empty
|
|
95
|
-
|
|
96
|
-
'Field %s of type %s has no options; returning the original value', declared.label, cls.__name__
|
|
97
|
-
)
|
|
97
|
+
log_codec_missing_options(cls.__name__, field_label=declared.label)
|
|
98
98
|
return parsed
|
|
99
99
|
|
|
100
100
|
if len(parsed) != len(set(parsed)):
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from decimal import ROUND_DOWN, Context, Decimal, InvalidOperation
|
|
3
2
|
|
|
4
3
|
from excelalchemy.codecs.base import (
|
|
@@ -6,6 +5,7 @@ from excelalchemy.codecs.base import (
|
|
|
6
5
|
NormalizedImportValue,
|
|
7
6
|
WorkbookDisplayValue,
|
|
8
7
|
WorkbookInputValue,
|
|
8
|
+
codec_logger,
|
|
9
9
|
log_codec_parse_fallback,
|
|
10
10
|
)
|
|
11
11
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -24,7 +24,7 @@ def canonicalize_decimal(value: Decimal, digits_limit: int | None) -> Decimal:
|
|
|
24
24
|
context=Context(rounding=ROUND_DOWN),
|
|
25
25
|
)
|
|
26
26
|
except InvalidOperation as e:
|
|
27
|
-
|
|
27
|
+
codec_logger.warning('Codec Number detected precision loss while quantizing fraction_digits: %s', e)
|
|
28
28
|
return value
|
|
29
29
|
|
|
30
30
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from typing import cast
|
|
3
2
|
|
|
4
3
|
from excelalchemy._primitives.constants import MULTI_CHECKBOX_SEPARATOR
|
|
5
4
|
from excelalchemy._primitives.identity import OptionId
|
|
5
|
+
from excelalchemy.codecs.base import log_codec_option_resolution_fallback, log_codec_render_fallback
|
|
6
6
|
from excelalchemy.codecs.multi_checkbox import MultiCheckbox
|
|
7
7
|
from excelalchemy.codecs.radio import Radio
|
|
8
8
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -44,7 +44,7 @@ class SingleOrganization(Radio):
|
|
|
44
44
|
try:
|
|
45
45
|
return presentation.options_id_map(field_label=declared.label)[OptionId(value.strip())].name
|
|
46
46
|
except KeyError:
|
|
47
|
-
|
|
47
|
+
log_codec_option_resolution_fallback(cls.__name__, value, field_label=declared.label)
|
|
48
48
|
|
|
49
49
|
return value
|
|
50
50
|
|
|
@@ -88,7 +88,12 @@ class MultiOrganization(MultiCheckbox):
|
|
|
88
88
|
option_names = presentation.exchange_option_ids_to_names(option_ids, field_label=declared.label)
|
|
89
89
|
return MULTI_CHECKBOX_SEPARATOR.join(map(str, option_names))
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
log_codec_render_fallback(
|
|
92
|
+
cls.__name__,
|
|
93
|
+
value,
|
|
94
|
+
field_label=declared.label,
|
|
95
|
+
reason='The workbook value is not a string or a list of option ids',
|
|
96
|
+
)
|
|
92
97
|
return str(value)
|
|
93
98
|
|
|
94
99
|
@classmethod
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
1
|
from excelalchemy._primitives.constants import MULTI_CHECKBOX_SEPARATOR
|
|
4
2
|
from excelalchemy._primitives.identity import OptionId
|
|
5
|
-
from excelalchemy.codecs.base import
|
|
3
|
+
from excelalchemy.codecs.base import (
|
|
4
|
+
ExcelFieldCodec,
|
|
5
|
+
WorkbookDisplayValue,
|
|
6
|
+
WorkbookInputValue,
|
|
7
|
+
log_codec_missing_options,
|
|
8
|
+
log_codec_option_resolution_fallback,
|
|
9
|
+
)
|
|
6
10
|
from excelalchemy.exceptions import ProgrammaticError
|
|
7
11
|
from excelalchemy.i18n.messages import MessageKey
|
|
8
12
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
@@ -49,7 +53,7 @@ class Radio(ExcelFieldCodec, str):
|
|
|
49
53
|
declared = field_meta.declared
|
|
50
54
|
presentation = field_meta.presentation
|
|
51
55
|
if not presentation.options:
|
|
52
|
-
|
|
56
|
+
log_codec_missing_options(cls.__name__, field_label=declared.label)
|
|
53
57
|
|
|
54
58
|
return '\n'.join(
|
|
55
59
|
[
|
|
@@ -74,13 +78,7 @@ class Radio(ExcelFieldCodec, str):
|
|
|
74
78
|
try:
|
|
75
79
|
return presentation.options_id_map(field_label=declared.label)[value.strip()].name
|
|
76
80
|
except Exception as exc:
|
|
77
|
-
|
|
78
|
-
'Type %s could not resolve option %s for field %s; returning the original value. Reason: %s',
|
|
79
|
-
cls.__name__,
|
|
80
|
-
value,
|
|
81
|
-
declared.label,
|
|
82
|
-
exc,
|
|
83
|
-
)
|
|
81
|
+
log_codec_option_resolution_fallback(cls.__name__, value, field_label=declared.label, exc=exc)
|
|
84
82
|
return value if value is not None else ''
|
|
85
83
|
|
|
86
84
|
@classmethod
|
|
@@ -96,9 +94,7 @@ class Radio(ExcelFieldCodec, str):
|
|
|
96
94
|
raise ProgrammaticError(msg(MessageKey.OPTIONS_CANNOT_BE_NONE_FOR_SELECTION_FIELDS))
|
|
97
95
|
|
|
98
96
|
if not presentation.options: # empty
|
|
99
|
-
|
|
100
|
-
'Field %s of type %s has no options; returning the original value', declared.label, cls.__name__
|
|
101
|
-
)
|
|
97
|
+
log_codec_missing_options(cls.__name__, field_label=declared.label)
|
|
102
98
|
return parsed
|
|
103
99
|
|
|
104
100
|
options_id_map = presentation.options_id_map(field_label=declared.label)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from typing import cast
|
|
3
2
|
|
|
4
3
|
from excelalchemy._primitives.constants import MULTI_CHECKBOX_SEPARATOR
|
|
5
4
|
from excelalchemy._primitives.identity import OptionId
|
|
5
|
+
from excelalchemy.codecs.base import log_codec_option_resolution_fallback, log_codec_render_fallback
|
|
6
6
|
from excelalchemy.codecs.multi_checkbox import MultiCheckbox
|
|
7
7
|
from excelalchemy.codecs.radio import Radio
|
|
8
8
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -45,12 +45,7 @@ class SingleStaff(Radio):
|
|
|
45
45
|
try:
|
|
46
46
|
return presentation.options_id_map(field_label=declared.label)[OptionId(value.strip())].name
|
|
47
47
|
except KeyError:
|
|
48
|
-
|
|
49
|
-
'Type %s could not resolve option %s for field %s; returning the original value',
|
|
50
|
-
cls.__name__,
|
|
51
|
-
value,
|
|
52
|
-
declared.label,
|
|
53
|
-
)
|
|
48
|
+
log_codec_option_resolution_fallback(cls.__name__, value, field_label=declared.label)
|
|
54
49
|
return value
|
|
55
50
|
|
|
56
51
|
|
|
@@ -97,7 +92,12 @@ class MultiStaff(MultiCheckbox):
|
|
|
97
92
|
option_names = presentation.exchange_option_ids_to_names(option_ids, field_label=declared.label)
|
|
98
93
|
return f'{MULTI_CHECKBOX_SEPARATOR}'.join(option_names)
|
|
99
94
|
|
|
100
|
-
|
|
95
|
+
log_codec_render_fallback(
|
|
96
|
+
cls.__name__,
|
|
97
|
+
value,
|
|
98
|
+
field_label=declared.label,
|
|
99
|
+
reason='The workbook value is not a string or a list of option ids',
|
|
100
|
+
)
|
|
101
101
|
return str(value)
|
|
102
102
|
|
|
103
103
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from excelalchemy.codecs.base import (
|
|
2
|
+
WorkbookDisplayValue,
|
|
3
|
+
WorkbookInputValue,
|
|
4
|
+
log_codec_option_resolution_fallback,
|
|
5
|
+
)
|
|
4
6
|
from excelalchemy.codecs.multi_checkbox import MultiCheckbox
|
|
5
7
|
from excelalchemy.codecs.radio import Radio
|
|
6
8
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -43,7 +45,7 @@ class SingleTreeNode(Radio):
|
|
|
43
45
|
try:
|
|
44
46
|
return presentation.options_id_map(field_label=declared.label)[value.strip()].name
|
|
45
47
|
except KeyError:
|
|
46
|
-
|
|
48
|
+
log_codec_option_resolution_fallback(cls.__name__, value, field_label=declared.label)
|
|
47
49
|
|
|
48
50
|
return value if value is not None else ''
|
|
49
51
|
|
|
@@ -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)
|
|
@@ -30,10 +30,22 @@ class ExcelAlchemyError(Exception):
|
|
|
30
30
|
def __str__(self) -> str:
|
|
31
31
|
return self.message
|
|
32
32
|
|
|
33
|
+
@property
|
|
34
|
+
def display_message(self) -> str:
|
|
35
|
+
return self.message
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def code(self) -> str:
|
|
39
|
+
if self.message_key is not None:
|
|
40
|
+
return self.message_key.value
|
|
41
|
+
return type(self).__name__
|
|
42
|
+
|
|
33
43
|
def to_dict(self) -> dict[str, object]:
|
|
34
44
|
payload: dict[str, object] = {
|
|
35
45
|
'type': type(self).__name__,
|
|
46
|
+
'code': self.code,
|
|
36
47
|
'message': self.message,
|
|
48
|
+
'display_message': self.display_message,
|
|
37
49
|
}
|
|
38
50
|
if self.message_key is not None:
|
|
39
51
|
payload['message_key'] = self.message_key.value
|
|
@@ -66,6 +78,10 @@ class ExcelCellError(ExcelAlchemyError):
|
|
|
66
78
|
def __str__(self) -> str:
|
|
67
79
|
return f'【{self.label}】{self.message}'
|
|
68
80
|
|
|
81
|
+
@property
|
|
82
|
+
def display_message(self) -> str:
|
|
83
|
+
return str(self)
|
|
84
|
+
|
|
69
85
|
def __repr__(self) -> str:
|
|
70
86
|
return (
|
|
71
87
|
f"{type(self).__name__}(label=Label('{self.label}'), "
|
|
@@ -103,6 +119,7 @@ class ExcelCellError(ExcelAlchemyError):
|
|
|
103
119
|
def to_dict(self) -> dict[str, object]:
|
|
104
120
|
payload = super().to_dict()
|
|
105
121
|
payload['label'] = str(self.label)
|
|
122
|
+
payload['field_label'] = str(self.label)
|
|
106
123
|
payload['parent_label'] = None if self.parent_label is None else str(self.parent_label)
|
|
107
124
|
payload['unique_label'] = str(self.unique_label)
|
|
108
125
|
return payload
|