Skip to content

Commit b69f155

Browse files
committed
feat(v2.1): add ImportSession
1 parent 26ada7b commit b69f155

19 files changed

Lines changed: 1163 additions & 427 deletions

CHANGELOG.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,57 @@ All notable changes to this project will be documented in this file.
44

55
The format is inspired by Keep a Changelog and versioned according to PEP 440.
66

7+
## [2.1.0] - Unreleased
8+
9+
This release continues the 2.x line with internal architecture cleanup, naming
10+
improvements, and stronger separation between long-lived facade state and
11+
single-run import workflow state.
12+
13+
### Added
14+
15+
- Added `ImportSession` as the one-shot import workflow runtime that keeps
16+
worksheet state, header parsing state, issue tracking, and executor state out
17+
of the long-lived `ExcelAlchemy` facade
18+
- Added normalized internal config layers:
19+
`ImporterSchemaOptions`, `ImportBehavior`, `ExporterSchemaOptions`,
20+
`ExportBehavior`, and `StorageOptions`
21+
- Added internal metadata layers:
22+
`DeclaredFieldMeta`, `RuntimeFieldBinding`, `WorkbookPresentationMeta`, and
23+
`ImportConstraints`
24+
- Added targeted regression tests for config normalization and split metadata
25+
layering
26+
27+
### Changed
28+
29+
- Refactored `ExcelAlchemy.import_data()` to delegate import execution to
30+
`ImportSession` while keeping the 2.x public API unchanged
31+
- Reduced facade responsibility so `ExcelAlchemy` now primarily owns durable
32+
configuration and collaborator wiring rather than import-run state
33+
- Normalized storage resolution so internal code now reads `storage_options`
34+
instead of scattering direct reads of `storage`, `minio`, `bucket_name`, and
35+
`url_expires`
36+
- Updated import execution to consume normalized `schema_options` and
37+
`behavior` objects instead of treating config as an all-in-one bus
38+
- Split `FieldMetaInfo` into internal declaration, runtime, presentation, and
39+
constraint layers while preserving the existing `FieldMeta(...)` and
40+
`ExcelMeta(...)` entry points
41+
- Improved internal naming by moving the primary worksheet variable vocabulary
42+
from `df/header_df` to `worksheet_table/header_table`
43+
- Renamed `excelalchemy.util.convertor` to
44+
`excelalchemy.util.converter`; the old path now remains as a deprecated
45+
compatibility shim
46+
- Simplified generic type parameter names across the config, facade, storage,
47+
executor, and import-session layers to better reflect their roles
48+
49+
### Compatibility Notes
50+
51+
- `df` and `header_df` remain available as backward-compatible aliases on the
52+
facade and import session, but new internal code should prefer
53+
`worksheet_table` and `header_table`
54+
- `excelalchemy.util.convertor` remains importable in 2.x and now emits the
55+
standard deprecation warning that points to `excelalchemy.util.converter`
56+
- No public import/export workflow API was removed in this release
57+
758
## [2.0.0.post1] - 2026-03-28
859

960
This post-release updates the package presentation and release metadata for the

docs/releases/2.1.0.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# 2.1.0 Release Checklist
2+
3+
This checklist is intended for the first feature release on top of the stable
4+
2.0 line.
5+
6+
## Purpose
7+
8+
- publish the first 2.1 release of ExcelAlchemy
9+
- ship the sessionized import workflow architecture
10+
- ship internal config normalization and metadata layering without breaking the
11+
public 2.x API
12+
- confirm that naming cleanup and compatibility shims are documented clearly
13+
14+
## Before Tagging
15+
16+
1. Confirm the intended version in `src/excelalchemy/__init__.py`.
17+
2. Review the `2.1.0` section in `CHANGELOG.md`.
18+
3. Confirm `README.md`, `README-pypi.md`, and `MIGRATIONS.md` still describe the
19+
recommended public paths correctly.
20+
4. Confirm the compatibility notes for:
21+
- `excelalchemy.util.convertor`
22+
- `df/header_df` compatibility aliases
23+
- legacy Minio config path
24+
25+
## Local Verification
26+
27+
Run these commands from the repository root:
28+
29+
```bash
30+
uv sync --extra development
31+
uv run ruff check .
32+
uv run pyright
33+
uv run pytest tests
34+
rm -rf dist
35+
uv build
36+
uvx twine check dist/*
37+
```
38+
39+
Optional smoke tests:
40+
41+
```bash
42+
uv venv .pkg-smoke-base --python 3.14
43+
uv pip install --python .pkg-smoke-base/bin/python dist/*.whl
44+
.pkg-smoke-base/bin/python -c "import excelalchemy; print(excelalchemy.__version__)"
45+
```
46+
47+
```bash
48+
uv venv .pkg-smoke-minio --python 3.14
49+
uv pip install --python .pkg-smoke-minio/bin/python "dist/*.whl[minio]"
50+
.pkg-smoke-minio/bin/python -c "from excelalchemy.core.storage_minio import MinioStorageGateway; print(MinioStorageGateway.__name__)"
51+
```
52+
53+
## GitHub Release Steps
54+
55+
1. Push the release commit to the default branch.
56+
2. In GitHub Releases, draft a new release.
57+
3. Create a new tag: `v2.1.0`.
58+
4. Use the `2.1.0` section from `CHANGELOG.md` as the release notes base.
59+
5. Publish the release and monitor the `Upload Python Package` workflow.
60+
61+
## Release Focus
62+
63+
When reviewing the final release notes, make sure they communicate these three
64+
themes clearly:
65+
66+
- `ExcelAlchemy` is now a lighter facade because import runtime state moved into
67+
`ImportSession`
68+
- config is still easy to construct publicly, but internally it is now split
69+
into schema, behavior, and storage layers
70+
- metadata now has clearer internal layering without forcing users to rewrite
71+
their `FieldMeta(...)` or `ExcelMeta(...)` declarations
72+
73+
## PyPI Verification
74+
75+
After the workflow completes:
76+
77+
1. Confirm the new release appears on PyPI.
78+
2. Confirm the long description renders correctly.
79+
3. Confirm screenshots and absolute links still work on the PyPI project page.
80+
4. Test base install:
81+
82+
```bash
83+
pip install -U ExcelAlchemy
84+
```
85+
86+
5. Test optional Minio install:
87+
88+
```bash
89+
pip install -U "ExcelAlchemy[minio]"
90+
```
91+
92+
6. Run one template-generation example.
93+
7. Run one import flow and one export flow.

src/excelalchemy/config.py

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from collections.abc import Callable
6-
from dataclasses import dataclass
6+
from dataclasses import dataclass, field
77
from enum import StrEnum
88
from typing import TYPE_CHECKING, Self
99

@@ -15,7 +15,7 @@
1515
from excelalchemy.helper.pydantic import get_model_field_names
1616
from excelalchemy.i18n.messages import MessageKey
1717
from excelalchemy.i18n.messages import message as msg
18-
from excelalchemy.util.convertor import export_data_converter, import_data_converter
18+
from excelalchemy.util.converter import export_data_converter, import_data_converter
1919

2020
if TYPE_CHECKING:
2121
from minio import Minio
@@ -34,10 +34,67 @@ class ImportMode(StrEnum):
3434
CREATE_OR_UPDATE = 'CREATE_OR_UPDATE'
3535

3636

37+
@dataclass(slots=True, frozen=True)
38+
class StorageOptions:
39+
"""Normalized storage backend settings shared by importer and exporter configs."""
40+
41+
storage: ExcelStorage | None
42+
minio: Minio | None
43+
bucket_name: str
44+
url_expires: int
45+
46+
@property
47+
def has_explicit_storage(self) -> bool:
48+
return self.storage is not None
49+
50+
@property
51+
def has_legacy_minio(self) -> bool:
52+
return self.minio is not None
53+
54+
55+
@dataclass(slots=True, frozen=True)
56+
class ImporterSchemaOptions[ImportCreateModelT: BaseModel, ImportUpdateModelT: BaseModel]:
57+
"""Schema declaration and workbook presentation settings for imports."""
58+
59+
create_importer_model: type[ImportCreateModelT] | None
60+
update_importer_model: type[ImportUpdateModelT] | None
61+
sheet_name: str
62+
locale: str
63+
64+
65+
@dataclass(slots=True, frozen=True)
66+
class ImportBehavior[ContextT]:
67+
"""Execution callbacks and import workflow policy."""
68+
69+
data_converter: DataConverter | None
70+
creator: DmlCallback[ContextT] | None
71+
updater: DmlCallback[ContextT] | None
72+
context: ImportContext[ContextT]
73+
is_data_exist: ExistenceCheckCallback[ContextT] | None
74+
exec_formatter: Callable[[Exception], str]
75+
import_mode: ImportMode
76+
77+
78+
@dataclass(slots=True, frozen=True)
79+
class ExporterSchemaOptions[ExportModelT: BaseModel]:
80+
"""Schema declaration and workbook presentation settings for exports."""
81+
82+
exporter_model: type[ExportModelT]
83+
sheet_name: str
84+
locale: str
85+
86+
87+
@dataclass(slots=True, frozen=True)
88+
class ExportBehavior:
89+
"""Execution behavior used when rendering export rows."""
90+
91+
data_converter: DataConverter | None
92+
93+
3794
@dataclass(slots=True)
38-
class ImporterConfig[ContextT, ImporterCreateModelT: BaseModel, ImporterUpdateModelT: BaseModel]:
39-
create_importer_model: type[ImporterCreateModelT] | None = None
40-
update_importer_model: type[ImporterUpdateModelT] | None = None
95+
class ImporterConfig[ContextT, ImportCreateModelT: BaseModel, ImportUpdateModelT: BaseModel]:
96+
create_importer_model: type[ImportCreateModelT] | None = None
97+
update_importer_model: type[ImportUpdateModelT] | None = None
4198

4299
# The converter receives schema keys rather than workbook labels.
43100
data_converter: DataConverter | None = import_data_converter
@@ -57,6 +114,11 @@ class ImporterConfig[ContextT, ImporterCreateModelT: BaseModel, ImporterUpdateMo
57114
locale: str = 'zh-CN'
58115

59116
sheet_name: str = 'Sheet1'
117+
schema_options: ImporterSchemaOptions[ImportCreateModelT, ImportUpdateModelT] = field(
118+
init=False, repr=False
119+
)
120+
behavior: ImportBehavior[ContextT] = field(init=False, repr=False)
121+
storage_options: StorageOptions = field(init=False, repr=False)
60122

61123
def validate_model(self) -> Self:
62124
if self.import_mode not in ImportMode.__members__.values():
@@ -100,11 +162,32 @@ def _validate_create_or_update(self) -> None:
100162

101163
def __post_init__(self) -> None:
102164
self.validate_model()
165+
self.schema_options = ImporterSchemaOptions(
166+
create_importer_model=self.create_importer_model,
167+
update_importer_model=self.update_importer_model,
168+
sheet_name=self.sheet_name,
169+
locale=self.locale,
170+
)
171+
self.behavior = ImportBehavior(
172+
data_converter=self.data_converter,
173+
creator=self.creator,
174+
updater=self.updater,
175+
context=self.context,
176+
is_data_exist=self.is_data_exist,
177+
exec_formatter=self.exec_formatter,
178+
import_mode=self.import_mode,
179+
)
180+
self.storage_options = StorageOptions(
181+
storage=self.storage,
182+
minio=self.minio,
183+
bucket_name=self.bucket_name,
184+
url_expires=self.url_expires,
185+
)
103186

104187

105188
@dataclass(slots=True)
106-
class ExporterConfig[ExporterModelT: BaseModel]:
107-
exporter_model: type[ExporterModelT]
189+
class ExporterConfig[ExportModelT: BaseModel]:
190+
exporter_model: type[ExportModelT]
108191
# The converter receives schema keys rather than workbook labels.
109192
data_converter: DataConverter | None = export_data_converter
110193

@@ -115,6 +198,9 @@ class ExporterConfig[ExporterModelT: BaseModel]:
115198
locale: str = 'zh-CN'
116199

117200
sheet_name: str = 'Sheet1'
201+
schema_options: ExporterSchemaOptions[ExportModelT] = field(init=False, repr=False)
202+
behavior: ExportBehavior = field(init=False, repr=False)
203+
storage_options: StorageOptions = field(init=False, repr=False)
118204

119205
def validate_model(self) -> Self:
120206
if not self.exporter_model:
@@ -123,3 +209,15 @@ def validate_model(self) -> Self:
123209

124210
def __post_init__(self) -> None:
125211
self.validate_model()
212+
self.schema_options = ExporterSchemaOptions(
213+
exporter_model=self.exporter_model,
214+
sheet_name=self.sheet_name,
215+
locale=self.locale,
216+
)
217+
self.behavior = ExportBehavior(data_converter=self.data_converter)
218+
self.storage_options = StorageOptions(
219+
storage=self.storage,
220+
minio=self.minio,
221+
bucket_name=self.bucket_name,
222+
url_expires=self.url_expires,
223+
)

src/excelalchemy/core/abstract.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@
1111

1212
class ABCExcelAlchemy[
1313
ContextT,
14-
ImporterCreateModelT: BaseModel,
15-
ImporterUpdateModelT: BaseModel,
16-
CreateModelT: BaseModel,
17-
UpdateModelT: BaseModel,
18-
ExporterModelT: BaseModel,
14+
ImportCreateModelT: BaseModel,
15+
ImportUpdateModelT: BaseModel,
16+
ExportModelT: BaseModel,
1917
](ABC):
2018
@abstractmethod
2119
def download_template(self, sample_data: list[ExportRowPayload] | None = None) -> DataUrlStr:

0 commit comments

Comments
 (0)