ExcelAlchemy 2.2.3__tar.gz → 2.2.4__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.3 → excelalchemy-2.2.4}/PKG-INFO +1 -1
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/__init__.py +1 -1
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/constants.py +0 -3
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/identity.py +2 -4
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/base.py +36 -13
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/boolean.py +14 -8
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/date.py +42 -21
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/date_range.py +18 -13
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/money.py +17 -4
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/multi_checkbox.py +14 -8
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/number.py +51 -29
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/number_range.py +4 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/organization.py +14 -6
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/phone_number.py +2 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/radio.py +24 -17
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/staff.py +15 -7
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/string.py +17 -13
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/tree.py +20 -10
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/url.py +2 -3
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/alchemy.py +24 -10
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/import_session.py +20 -10
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/rows.py +6 -5
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/schema.py +21 -15
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/storage.py +2 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/writer.py +17 -11
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/exceptions.py +3 -5
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/helper/pydantic.py +19 -12
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/metadata.py +204 -91
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/file.py +2 -2
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/LICENSE +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/README-pypi.md +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/pyproject.toml +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/deprecation.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/header_models.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/_primitives/payloads.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/artifacts.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/codecs/email.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/config.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/const.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/abstract.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/executor.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/headers.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/rendering.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/storage_minio.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/storage_protocol.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/core/table.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/exc.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/header_models.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/helper/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/i18n/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/i18n/messages.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/identity.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/py.typed +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/results.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/abstract.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/alchemy.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/field.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/header.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/identity.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/result.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/boolean.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/date.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/date_range.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/email.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/money.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/multi_checkbox.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/number.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/number_range.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/organization.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/phone_number.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/radio.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/staff.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/string.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/tree.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/types/value/url.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/__init__.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/converter.py +0 -0
- {excelalchemy-2.2.3 → excelalchemy-2.2.4}/src/excelalchemy/util/convertor.py +0 -0
|
@@ -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.4'
|
|
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 (
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import StrEnum
|
|
3
|
-
from typing import Any
|
|
4
3
|
|
|
5
4
|
from excelalchemy._primitives.identity import Key, Label, OptionId
|
|
6
5
|
from excelalchemy.i18n.messages import MessageKey
|
|
@@ -38,8 +37,6 @@ MILLISECOND_TO_SECOND = 1000
|
|
|
38
37
|
MAX_OPTIONS_COUNT = 100
|
|
39
38
|
|
|
40
39
|
DEFAULT_FIELD_META_ORDER = -1
|
|
41
|
-
type DictStrAny = dict[str, Any]
|
|
42
|
-
type DictAny = dict[Any, Any]
|
|
43
40
|
type SetStr = set[str]
|
|
44
41
|
type ListStr = list[str]
|
|
45
42
|
type IntStr = int | str
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Internal typed primitives used across the ExcelAlchemy core layer."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
from pydantic import GetCoreSchemaHandler
|
|
6
4
|
from pydantic_core import core_schema
|
|
7
5
|
|
|
@@ -10,7 +8,7 @@ class _StringIdentity(str):
|
|
|
10
8
|
@classmethod
|
|
11
9
|
def __get_pydantic_core_schema__(
|
|
12
10
|
cls,
|
|
13
|
-
source_type:
|
|
11
|
+
source_type: object,
|
|
14
12
|
handler: GetCoreSchemaHandler,
|
|
15
13
|
) -> core_schema.CoreSchema:
|
|
16
14
|
return core_schema.no_info_after_validator_function(cls, core_schema.str_schema())
|
|
@@ -20,7 +18,7 @@ class _IntegerIdentity(int):
|
|
|
20
18
|
@classmethod
|
|
21
19
|
def __get_pydantic_core_schema__(
|
|
22
20
|
cls,
|
|
23
|
-
source_type:
|
|
21
|
+
source_type: object,
|
|
24
22
|
handler: GetCoreSchemaHandler,
|
|
25
23
|
) -> core_schema.CoreSchema:
|
|
26
24
|
return core_schema.no_info_after_validator_function(cls, core_schema.int_schema())
|
|
@@ -11,6 +11,13 @@ from excelalchemy._primitives.identity import Key
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from excelalchemy.metadata import FieldMetaInfo
|
|
13
13
|
|
|
14
|
+
# These aliases remain `Any` intentionally because codec subclasses narrow their
|
|
15
|
+
# accepted workbook values heavily. Using `object` here makes every override
|
|
16
|
+
# incompatible under pyright's method override rules.
|
|
17
|
+
type WorkbookInputValue = Any
|
|
18
|
+
type WorkbookDisplayValue = Any
|
|
19
|
+
type NormalizedImportValue = Any
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
class ExcelFieldCodec(ABC):
|
|
16
23
|
"""Excel-facing field adapter responsible for comments, parsing, formatting, and normalization."""
|
|
@@ -22,17 +29,17 @@ class ExcelFieldCodec(ABC):
|
|
|
22
29
|
|
|
23
30
|
@classmethod
|
|
24
31
|
@abstractmethod
|
|
25
|
-
def parse_input(cls, value:
|
|
32
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
26
33
|
"""Parse workbook input into the intermediate Python value consumed by the import pipeline."""
|
|
27
34
|
|
|
28
35
|
@classmethod
|
|
29
36
|
@abstractmethod
|
|
30
|
-
def format_display_value(cls, value:
|
|
37
|
+
def format_display_value(cls, value: WorkbookDisplayValue, field_meta: FieldMetaInfo) -> WorkbookDisplayValue:
|
|
31
38
|
"""Format a raw worksheet value back into a user-recognizable display value."""
|
|
32
39
|
|
|
33
40
|
@classmethod
|
|
34
41
|
@abstractmethod
|
|
35
|
-
def normalize_import_value(cls, value:
|
|
42
|
+
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
36
43
|
"""Validate and normalize parsed input before handing it to the Pydantic layer."""
|
|
37
44
|
|
|
38
45
|
@classmethod
|
|
@@ -41,24 +48,24 @@ class ExcelFieldCodec(ABC):
|
|
|
41
48
|
return cls.build_comment(field_meta)
|
|
42
49
|
|
|
43
50
|
@classmethod
|
|
44
|
-
def serialize(cls, value:
|
|
51
|
+
def serialize(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
45
52
|
"""Backward-compatible alias for parse_input()."""
|
|
46
53
|
return cls.parse_input(value, field_meta)
|
|
47
54
|
|
|
48
55
|
@classmethod
|
|
49
|
-
def deserialize(cls, value:
|
|
56
|
+
def deserialize(cls, value: WorkbookDisplayValue, field_meta: FieldMetaInfo) -> WorkbookDisplayValue:
|
|
50
57
|
"""Backward-compatible alias for format_display_value()."""
|
|
51
58
|
return cls.format_display_value(value, field_meta)
|
|
52
59
|
|
|
53
60
|
@classmethod
|
|
54
|
-
def __validate__(cls, value:
|
|
61
|
+
def __validate__(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
55
62
|
"""Backward-compatible alias for normalize_import_value()."""
|
|
56
63
|
return cls.normalize_import_value(value, field_meta)
|
|
57
64
|
|
|
58
65
|
@classmethod
|
|
59
66
|
def __get_pydantic_core_schema__(
|
|
60
67
|
cls,
|
|
61
|
-
source_type:
|
|
68
|
+
source_type: object,
|
|
62
69
|
handler: GetCoreSchemaHandler,
|
|
63
70
|
) -> core_schema.CoreSchema:
|
|
64
71
|
# ExcelAlchemy runs metadata-aware validation in its adapter layer.
|
|
@@ -88,15 +95,23 @@ class SystemReserved(ExcelFieldCodec):
|
|
|
88
95
|
return ''
|
|
89
96
|
|
|
90
97
|
@classmethod
|
|
91
|
-
def parse_input(cls, value:
|
|
98
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
92
99
|
return value
|
|
93
100
|
|
|
94
101
|
@classmethod
|
|
95
|
-
def format_display_value(
|
|
102
|
+
def format_display_value(
|
|
103
|
+
cls,
|
|
104
|
+
value: WorkbookDisplayValue,
|
|
105
|
+
field_meta: FieldMetaInfo,
|
|
106
|
+
) -> WorkbookDisplayValue:
|
|
96
107
|
return value
|
|
97
108
|
|
|
98
109
|
@classmethod
|
|
99
|
-
def normalize_import_value(
|
|
110
|
+
def normalize_import_value(
|
|
111
|
+
cls,
|
|
112
|
+
value: WorkbookInputValue,
|
|
113
|
+
field_meta: FieldMetaInfo,
|
|
114
|
+
) -> NormalizedImportValue:
|
|
100
115
|
return value
|
|
101
116
|
|
|
102
117
|
|
|
@@ -108,15 +123,23 @@ class Undefined(ExcelFieldCodec):
|
|
|
108
123
|
return ''
|
|
109
124
|
|
|
110
125
|
@classmethod
|
|
111
|
-
def parse_input(cls, value:
|
|
126
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> WorkbookInputValue:
|
|
112
127
|
return value
|
|
113
128
|
|
|
114
129
|
@classmethod
|
|
115
|
-
def format_display_value(
|
|
130
|
+
def format_display_value(
|
|
131
|
+
cls,
|
|
132
|
+
value: WorkbookDisplayValue,
|
|
133
|
+
field_meta: FieldMetaInfo,
|
|
134
|
+
) -> WorkbookDisplayValue:
|
|
116
135
|
return value
|
|
117
136
|
|
|
118
137
|
@classmethod
|
|
119
|
-
def normalize_import_value(
|
|
138
|
+
def normalize_import_value(
|
|
139
|
+
cls,
|
|
140
|
+
value: WorkbookInputValue,
|
|
141
|
+
field_meta: FieldMetaInfo,
|
|
142
|
+
) -> NormalizedImportValue:
|
|
120
143
|
return value
|
|
121
144
|
|
|
122
145
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Any
|
|
3
2
|
|
|
4
3
|
from excelalchemy.codecs import excel_choice_codec
|
|
5
|
-
from excelalchemy.codecs.base import ExcelFieldCodec
|
|
4
|
+
from excelalchemy.codecs.base import ExcelFieldCodec, WorkbookDisplayValue, WorkbookInputValue
|
|
6
5
|
from excelalchemy.i18n.messages import MessageKey
|
|
7
6
|
from excelalchemy.i18n.messages import display_message as dmsg
|
|
8
7
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -31,19 +30,26 @@ class Boolean(ExcelFieldCodec):
|
|
|
31
30
|
|
|
32
31
|
@classmethod
|
|
33
32
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
33
|
+
declared = field_meta.declared
|
|
34
|
+
presentation = field_meta.presentation
|
|
34
35
|
return '\n'.join(
|
|
35
36
|
[
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
declared.comment_required,
|
|
38
|
+
presentation.comment_hint,
|
|
38
39
|
]
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
@classmethod
|
|
42
|
-
def parse_input(cls, value:
|
|
43
|
+
def parse_input(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> str:
|
|
43
44
|
return str(value).strip()
|
|
44
45
|
|
|
45
46
|
@classmethod
|
|
46
|
-
def format_display_value(
|
|
47
|
+
def format_display_value(
|
|
48
|
+
cls,
|
|
49
|
+
value: bool | str | WorkbookDisplayValue | None,
|
|
50
|
+
field_meta: FieldMetaInfo,
|
|
51
|
+
) -> str:
|
|
52
|
+
declared = field_meta.declared
|
|
47
53
|
if value is None or value == '':
|
|
48
54
|
return cls._false_display()
|
|
49
55
|
|
|
@@ -64,14 +70,14 @@ class Boolean(ExcelFieldCodec):
|
|
|
64
70
|
'Type %s could not deserialize %s for field %s; returning the default value %s',
|
|
65
71
|
cls.__name__,
|
|
66
72
|
value,
|
|
67
|
-
|
|
73
|
+
declared.label,
|
|
68
74
|
cls._false_display(),
|
|
69
75
|
)
|
|
70
76
|
|
|
71
77
|
return cls._true_display() if str(value) in cls._true_values() else cls._false_display()
|
|
72
78
|
|
|
73
79
|
@classmethod
|
|
74
|
-
def normalize_import_value(cls, value: str | bool |
|
|
80
|
+
def normalize_import_value(cls, value: str | bool | WorkbookInputValue, field_meta: FieldMetaInfo) -> bool:
|
|
75
81
|
if isinstance(value, bool):
|
|
76
82
|
return value
|
|
77
83
|
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import cast
|
|
4
4
|
|
|
5
5
|
import pendulum
|
|
6
6
|
from pendulum import DateTime
|
|
7
7
|
|
|
8
8
|
from excelalchemy._primitives.constants import DATE_FORMAT_TO_HINT_MAPPING, MILLISECOND_TO_SECOND, DataRangeOption
|
|
9
|
-
from excelalchemy.codecs.base import
|
|
9
|
+
from excelalchemy.codecs.base import (
|
|
10
|
+
ExcelFieldCodec,
|
|
11
|
+
NormalizedImportValue,
|
|
12
|
+
WorkbookDisplayValue,
|
|
13
|
+
WorkbookInputValue,
|
|
14
|
+
)
|
|
10
15
|
from excelalchemy.exceptions import ConfigError
|
|
11
16
|
from excelalchemy.i18n.messages import MessageKey
|
|
12
17
|
from excelalchemy.i18n.messages import message as msg
|
|
@@ -18,36 +23,44 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
18
23
|
|
|
19
24
|
@classmethod
|
|
20
25
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
21
|
-
|
|
26
|
+
declared = field_meta.declared
|
|
27
|
+
presentation = field_meta.presentation
|
|
28
|
+
if not presentation.date_format:
|
|
22
29
|
raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
23
30
|
return '\n'.join(
|
|
24
31
|
[
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
declared.comment_required,
|
|
33
|
+
presentation.comment_date_format,
|
|
34
|
+
presentation.comment_date_range_option,
|
|
35
|
+
presentation.comment_hint,
|
|
29
36
|
]
|
|
30
37
|
)
|
|
31
38
|
|
|
32
39
|
@classmethod
|
|
33
|
-
def parse_input(
|
|
40
|
+
def parse_input(
|
|
41
|
+
cls,
|
|
42
|
+
value: str | DateTime | WorkbookInputValue,
|
|
43
|
+
field_meta: FieldMetaInfo,
|
|
44
|
+
) -> datetime | WorkbookInputValue:
|
|
45
|
+
declared = field_meta.declared
|
|
46
|
+
presentation = field_meta.presentation
|
|
34
47
|
if isinstance(value, DateTime):
|
|
35
48
|
logging.info(
|
|
36
49
|
'Codec %s received a parsed datetime for %s; returning it unchanged: %s',
|
|
37
50
|
cls.__name__,
|
|
38
|
-
|
|
51
|
+
declared.label,
|
|
39
52
|
value,
|
|
40
53
|
)
|
|
41
54
|
return value
|
|
42
55
|
|
|
43
|
-
if not
|
|
56
|
+
if not presentation.date_format:
|
|
44
57
|
raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
45
58
|
|
|
46
59
|
value = str(value).strip()
|
|
47
60
|
try:
|
|
48
61
|
v = value.replace('/', '-') # pendulum does not accept "/" as a date separator here.
|
|
49
62
|
dt: DateTime = cast(DateTime, pendulum.parse(v))
|
|
50
|
-
return dt.replace(tzinfo=
|
|
63
|
+
return dt.replace(tzinfo=presentation.timezone)
|
|
51
64
|
except Exception as exc:
|
|
52
65
|
logging.warning(
|
|
53
66
|
'ValueType <%s> could not parse Excel input %s; returning the original value. Reason: %s',
|
|
@@ -58,27 +71,33 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
58
71
|
return value
|
|
59
72
|
|
|
60
73
|
@classmethod
|
|
61
|
-
def format_display_value(
|
|
74
|
+
def format_display_value(
|
|
75
|
+
cls,
|
|
76
|
+
value: str | datetime | WorkbookDisplayValue | None,
|
|
77
|
+
field_meta: FieldMetaInfo,
|
|
78
|
+
) -> str:
|
|
79
|
+
presentation = field_meta.presentation
|
|
62
80
|
match value:
|
|
63
81
|
case None | '':
|
|
64
82
|
return ''
|
|
65
83
|
case datetime():
|
|
66
|
-
return value.strftime(
|
|
84
|
+
return value.strftime(presentation.python_date_format)
|
|
67
85
|
case int() | float():
|
|
68
86
|
return datetime.fromtimestamp(int(value) / MILLISECOND_TO_SECOND).strftime(
|
|
69
|
-
|
|
87
|
+
presentation.python_date_format
|
|
70
88
|
)
|
|
71
89
|
case _:
|
|
72
90
|
return str(value) if value is not None else ''
|
|
73
91
|
|
|
74
92
|
@classmethod
|
|
75
|
-
def normalize_import_value(cls, value:
|
|
76
|
-
|
|
93
|
+
def normalize_import_value(cls, value: WorkbookInputValue, field_meta: FieldMetaInfo) -> NormalizedImportValue:
|
|
94
|
+
presentation = field_meta.presentation
|
|
95
|
+
if presentation.date_format is None:
|
|
77
96
|
raise ConfigError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
78
97
|
|
|
79
98
|
if not isinstance(value, datetime):
|
|
80
99
|
raise ValueError(
|
|
81
|
-
msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[
|
|
100
|
+
msg(MessageKey.ENTER_DATE_FORMAT, date_format=DATE_FORMAT_TO_HINT_MAPPING[presentation.date_format])
|
|
82
101
|
)
|
|
83
102
|
|
|
84
103
|
parsed = cls._parse_date(value, field_meta)
|
|
@@ -91,17 +110,19 @@ class Date(ExcelFieldCodec, datetime):
|
|
|
91
110
|
|
|
92
111
|
@staticmethod
|
|
93
112
|
def _parse_date(v: datetime, field_meta: FieldMetaInfo) -> datetime:
|
|
94
|
-
|
|
113
|
+
presentation = field_meta.presentation
|
|
114
|
+
format_ = presentation.python_date_format
|
|
95
115
|
parsed = datetime.strptime(v.strftime(format_), format_)
|
|
96
|
-
parsed = parsed.replace(tzinfo=
|
|
116
|
+
parsed = parsed.replace(tzinfo=presentation.timezone)
|
|
97
117
|
return parsed
|
|
98
118
|
|
|
99
119
|
@staticmethod
|
|
100
120
|
def _validate_date_range(parsed: datetime, field_meta: FieldMetaInfo) -> list[str]:
|
|
101
|
-
|
|
121
|
+
presentation = field_meta.presentation
|
|
122
|
+
now = datetime.now(tz=presentation.timezone)
|
|
102
123
|
errors: list[str] = []
|
|
103
124
|
|
|
104
|
-
match
|
|
125
|
+
match presentation.date_range_option:
|
|
105
126
|
case DataRangeOption.PRE:
|
|
106
127
|
if parsed > now:
|
|
107
128
|
errors.append(msg(MessageKey.DATE_MUST_BE_EARLIER_THAN_NOW))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections.abc import Mapping
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import cast
|
|
5
5
|
|
|
6
6
|
import pendulum
|
|
7
7
|
from pendulum import DateTime
|
|
@@ -28,7 +28,7 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
28
28
|
__name__ = 'DateRange'
|
|
29
29
|
|
|
30
30
|
@classmethod
|
|
31
|
-
def model_validate(cls, obj:
|
|
31
|
+
def model_validate(cls, obj: object) -> 'DateRange':
|
|
32
32
|
impl = _DateRangeImpl.model_validate(obj)
|
|
33
33
|
self = cls(impl.start, impl.end)
|
|
34
34
|
return self
|
|
@@ -50,14 +50,16 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
50
50
|
|
|
51
51
|
@classmethod
|
|
52
52
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
53
|
-
|
|
53
|
+
declared = field_meta.declared
|
|
54
|
+
presentation = field_meta.presentation
|
|
55
|
+
if presentation.date_format is None:
|
|
54
56
|
raise RuntimeError(msg(MessageKey.DATE_FORMAT_NOT_CONFIGURED))
|
|
55
57
|
|
|
56
58
|
return '\n'.join(
|
|
57
59
|
[
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
dmsg(MessageKey.COMMENT_DATE_RANGE_START_NOT_AFTER_END, extra_hint=
|
|
60
|
+
declared.comment_required,
|
|
61
|
+
presentation.comment_date_format,
|
|
62
|
+
dmsg(MessageKey.COMMENT_DATE_RANGE_START_NOT_AFTER_END, extra_hint=presentation.hint or ''),
|
|
61
63
|
]
|
|
62
64
|
)
|
|
63
65
|
|
|
@@ -92,20 +94,21 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
92
94
|
value: object,
|
|
93
95
|
field_meta: FieldMetaInfo,
|
|
94
96
|
) -> 'DateRange':
|
|
97
|
+
presentation = field_meta.presentation
|
|
95
98
|
try:
|
|
96
99
|
parsed = DateRange.model_validate(value)
|
|
97
|
-
parsed.start = pendulum.instance(parsed.start, tz=
|
|
98
|
-
parsed.end = pendulum.instance(parsed.end, tz=
|
|
100
|
+
parsed.start = pendulum.instance(parsed.start, tz=presentation.timezone) if parsed.start else None
|
|
101
|
+
parsed.end = pendulum.instance(parsed.end, tz=presentation.timezone) if parsed.end else None
|
|
99
102
|
except Exception as exc:
|
|
100
103
|
raise ValueError(msg(MessageKey.INVALID_INPUT)) from exc
|
|
101
104
|
|
|
102
105
|
errors: list[str] = []
|
|
103
|
-
now = datetime.now(tz=
|
|
106
|
+
now = datetime.now(tz=presentation.timezone)
|
|
104
107
|
|
|
105
108
|
if parsed.start and parsed.end and parsed.start > parsed.end:
|
|
106
109
|
errors.append(msg(MessageKey.DATE_RANGE_START_AFTER_END))
|
|
107
110
|
|
|
108
|
-
match
|
|
111
|
+
match presentation.date_range_option:
|
|
109
112
|
case DataRangeOption.PRE:
|
|
110
113
|
if (parsed.start and parsed.start > now) or (parsed.end and parsed.end > now):
|
|
111
114
|
errors.append(msg(MessageKey.DATE_MUST_BE_EARLIER_THAN_NOW))
|
|
@@ -124,7 +127,8 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
124
127
|
def format_display_value(cls, value: object | None, field_meta: FieldMetaInfo) -> str:
|
|
125
128
|
if value is None or value == '':
|
|
126
129
|
return ''
|
|
127
|
-
|
|
130
|
+
presentation = field_meta.presentation
|
|
131
|
+
date_format = presentation.must_date_format
|
|
128
132
|
py_date_format = DATE_FORMAT_TO_PYTHON_MAPPING[date_format]
|
|
129
133
|
|
|
130
134
|
if isinstance(value, str):
|
|
@@ -178,11 +182,12 @@ class DateRange(CompositeExcelFieldCodec):
|
|
|
178
182
|
|
|
179
183
|
@staticmethod
|
|
180
184
|
def _parse_datetime_text(value: str, field_meta: FieldMetaInfo) -> DateTime:
|
|
185
|
+
presentation = field_meta.presentation
|
|
181
186
|
parsed = pendulum.parse(value)
|
|
182
187
|
if isinstance(parsed, DateTime):
|
|
183
|
-
return parsed.replace(tzinfo=
|
|
188
|
+
return parsed.replace(tzinfo=presentation.timezone)
|
|
184
189
|
if isinstance(parsed, datetime):
|
|
185
|
-
return pendulum.instance(parsed).replace(tzinfo=
|
|
190
|
+
return pendulum.instance(parsed).replace(tzinfo=presentation.timezone)
|
|
186
191
|
raise ValueError(msg(MessageKey.INVALID_INPUT))
|
|
187
192
|
|
|
188
193
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import replace
|
|
2
|
+
from typing import ClassVar
|
|
2
3
|
|
|
4
|
+
from excelalchemy.codecs.base import NormalizedImportValue, WorkbookDisplayValue, WorkbookInputValue
|
|
3
5
|
from excelalchemy.codecs.number import Number
|
|
4
6
|
from excelalchemy.metadata import FieldMetaInfo
|
|
5
7
|
|
|
@@ -10,7 +12,10 @@ class Money(Number):
|
|
|
10
12
|
@classmethod
|
|
11
13
|
def _money_field_meta(cls, field_meta: FieldMetaInfo) -> FieldMetaInfo:
|
|
12
14
|
money_field_meta = field_meta.clone()
|
|
13
|
-
money_field_meta.
|
|
15
|
+
money_field_meta.presentation_meta = replace(
|
|
16
|
+
money_field_meta.presentation_meta,
|
|
17
|
+
fraction_digits=cls.MONEY_FRACTION_DIGITS,
|
|
18
|
+
)
|
|
14
19
|
return money_field_meta
|
|
15
20
|
|
|
16
21
|
@classmethod
|
|
@@ -18,11 +23,19 @@ class Money(Number):
|
|
|
18
23
|
return super().build_comment(cls._money_field_meta(field_meta))
|
|
19
24
|
|
|
20
25
|
@classmethod
|
|
21
|
-
def format_display_value(
|
|
26
|
+
def format_display_value(
|
|
27
|
+
cls,
|
|
28
|
+
value: str | WorkbookDisplayValue | None,
|
|
29
|
+
field_meta: FieldMetaInfo,
|
|
30
|
+
) -> str:
|
|
22
31
|
return super().format_display_value(value, cls._money_field_meta(field_meta))
|
|
23
32
|
|
|
24
33
|
@classmethod
|
|
25
|
-
def normalize_import_value(
|
|
34
|
+
def normalize_import_value(
|
|
35
|
+
cls,
|
|
36
|
+
value: WorkbookInputValue,
|
|
37
|
+
field_meta: FieldMetaInfo,
|
|
38
|
+
) -> NormalizedImportValue:
|
|
26
39
|
return super().normalize_import_value(value, cls._money_field_meta(field_meta))
|
|
27
40
|
|
|
28
41
|
|
|
@@ -16,12 +16,14 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
16
16
|
|
|
17
17
|
@classmethod
|
|
18
18
|
def build_comment(cls, field_meta: FieldMetaInfo) -> str:
|
|
19
|
+
declared = field_meta.declared
|
|
20
|
+
presentation = field_meta.presentation
|
|
19
21
|
return '\n'.join(
|
|
20
22
|
[
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
declared.comment_required,
|
|
24
|
+
presentation.comment_options,
|
|
23
25
|
dmsg(MessageKey.COMMENT_SELECTION_MODE, value=dmsg(MessageKey.COMMENT_SELECTION_VALUE_MULTI)),
|
|
24
|
-
|
|
26
|
+
presentation.comment_hint,
|
|
25
27
|
]
|
|
26
28
|
)
|
|
27
29
|
|
|
@@ -41,25 +43,27 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
41
43
|
|
|
42
44
|
@classmethod
|
|
43
45
|
def normalize_import_value(cls, value: object, field_meta: FieldMetaInfo) -> list[str]: # OptionId
|
|
46
|
+
declared = field_meta.declared
|
|
47
|
+
presentation = field_meta.presentation
|
|
44
48
|
if not isinstance(value, list):
|
|
45
49
|
raise ValueError(msg(MessageKey.OPTION_NOT_FOUND_HEADER_COMMENT))
|
|
46
50
|
|
|
47
51
|
items = cast(list[object], value)
|
|
48
52
|
parsed = [str(item).strip() for item in items]
|
|
49
53
|
|
|
50
|
-
if
|
|
54
|
+
if presentation.options is None:
|
|
51
55
|
raise ProgrammaticError(msg(MessageKey.OPTIONS_CANNOT_BE_NONE_FOR_VALUE_TYPE, value_type=cls.__name__))
|
|
52
56
|
|
|
53
|
-
if not
|
|
57
|
+
if not presentation.options: # empty
|
|
54
58
|
logging.warning(
|
|
55
|
-
'Field %s of type %s has no options; returning the original value',
|
|
59
|
+
'Field %s of type %s has no options; returning the original value', declared.label, cls.__name__
|
|
56
60
|
)
|
|
57
61
|
return parsed
|
|
58
62
|
|
|
59
63
|
if len(parsed) != len(set(parsed)):
|
|
60
64
|
raise ValueError(msg(MessageKey.OPTIONS_CONTAIN_DUPLICATES))
|
|
61
65
|
|
|
62
|
-
result, errors =
|
|
66
|
+
result, errors = presentation.exchange_names_to_option_ids_with_errors(parsed, field_label=declared.label)
|
|
63
67
|
|
|
64
68
|
if errors:
|
|
65
69
|
raise ValueError(*errors)
|
|
@@ -68,6 +72,8 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
68
72
|
|
|
69
73
|
@classmethod
|
|
70
74
|
def format_display_value(cls, value: str | list[OptionId] | None, field_meta: FieldMetaInfo) -> str:
|
|
75
|
+
declared = field_meta.declared
|
|
76
|
+
presentation = field_meta.presentation
|
|
71
77
|
match value:
|
|
72
78
|
case None | '':
|
|
73
79
|
return ''
|
|
@@ -75,7 +81,7 @@ class MultiCheckbox(ExcelFieldCodec, list[str]):
|
|
|
75
81
|
return value
|
|
76
82
|
case list():
|
|
77
83
|
option_ids = [OptionId(option_id) for option_id in value]
|
|
78
|
-
option_names =
|
|
84
|
+
option_names = presentation.exchange_option_ids_to_names(option_ids, field_label=declared.label)
|
|
79
85
|
return f'{MULTI_CHECKBOX_SEPARATOR}'.join(option_names)
|
|
80
86
|
|
|
81
87
|
|