sqlalchemy-excel 0.2.2__tar.gz → 0.3.1__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.
- sqlalchemy_excel-0.3.1/CHANGELOG.md +72 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/PKG-INFO +30 -1
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/README.md +27 -0
- sqlalchemy_excel-0.3.1/docs/DEVELOPMENT.md +263 -0
- sqlalchemy_excel-0.3.1/docs/ROADMAP.md +151 -0
- sqlalchemy_excel-0.3.1/docs/USAGE.md +267 -0
- sqlalchemy_excel-0.3.1/logo.svg +21 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/pyproject.toml +3 -1
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/__init__.py +3 -2
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/dialect.py +61 -0
- sqlalchemy_excel-0.3.1/tests/test_graph_dialect.py +170 -0
- sqlalchemy_excel-0.2.2/CHANGELOG.md +0 -13
- sqlalchemy_excel-0.2.2/logo.svg +0 -23
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.editorconfig +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/task.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/RELEASE_CHECKLIST.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/RELEASE_NOTES_TEMPLATE.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/dependabot.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/labels.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/release.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/workflows/ci.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/workflows/publish-pypi.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.gitignore +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.pre-commit-config.yaml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/CODE_OF_CONDUCT.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/CONTRIBUTING.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/LICENSE +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/Makefile +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/SECURITY.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/SUPPORT.md +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/cliff.toml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/codecov.yml +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/compiler.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/ddl.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/py.typed +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/reflection.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/src/sqlalchemy_excel/types.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/conftest.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_compiler.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_ddl.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_dialect.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_dml.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_orm.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_reflection.py +0 -0
- {sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/tests/test_types.py +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.1] - 2026-04-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Add explicit `supports_statement_cache = False` to `ExcelGraphDialect` to suppress SQLAlchemy caching warning
|
|
7
|
+
- Fix import ordering in test_graph_dialect.py for ruff I001 compliance
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-04-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `ExcelGraphDialect` for remote Excel files via Microsoft Graph API
|
|
13
|
+
- `excel+graph:///drive_id/item_id` URL scheme support
|
|
14
|
+
- Entry point `excel.graph` for SQLAlchemy dialect resolution
|
|
15
|
+
- Optional dependency: `pip install sqlalchemy-excel[graph]`
|
|
16
|
+
- URL percent-decoding for drive/item IDs with special characters
|
|
17
|
+
- `readonly` query parameter forwarding to Graph backend
|
|
18
|
+
- Comprehensive Graph dialect tests with `httpx.MockTransport`
|
|
19
|
+
- `docs/` directory with USAGE.md, DEVELOPMENT.md, and ROADMAP.md
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Version bumped to 0.3.0
|
|
23
|
+
|
|
24
|
+
## [0.2.2] - 2026-04-12
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Restored cast() calls needed for CI mypy and suppress redundant-cast locally
|
|
28
|
+
- Version bumped to 0.2.2
|
|
29
|
+
|
|
30
|
+
## [0.2.1] - 2026-04-12
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Project logo (modern minimalist SVG)
|
|
34
|
+
- Contributing guide, Code of Conduct, Security and Support policies
|
|
35
|
+
- Development tooling: Makefile, .editorconfig, pre-commit-config, codecov.yml, git-cliff config
|
|
36
|
+
- GitHub issue/PR templates and project management files
|
|
37
|
+
- py.typed marker for PEP 561 compliance
|
|
38
|
+
- twine check step in publish workflow
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Classifier updated from Alpha to Beta
|
|
42
|
+
- Changelog URL added to project metadata
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- Oracle review findings: rollback docs, absolute logo URLs, metadata alignment
|
|
46
|
+
|
|
47
|
+
## [0.2.0] - 2026-04-12
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
- Full dialect rewrite: ExcelCompiler, ExcelDDLCompiler, ExcelTypeCompiler, ExcelInspectionMixin
|
|
51
|
+
- Comprehensive README with ORM examples, type mapping table, schema inspection docs
|
|
52
|
+
- Test coverage reporting with Codecov CI integration
|
|
53
|
+
- IN, BETWEEN, LIKE operator tests for SQLAlchemy dialect
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- excel-dbapi dependency updated to >=0.2.0
|
|
57
|
+
- Version bumped to 0.2.0
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- mypy strict incompatibility with SQLAlchemy dialect overrides (temporarily disabled then re-enabled)
|
|
61
|
+
|
|
62
|
+
## [0.1.0] - 2026-04-12
|
|
63
|
+
|
|
64
|
+
- Initial release
|
|
65
|
+
- SQLAlchemy 2.0 dialect for Excel files
|
|
66
|
+
- PEP 249 DB-API 2.0 driver via excel-dbapi
|
|
67
|
+
- SQL support: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
|
|
68
|
+
- WHERE clause with AND/OR, comparison operators, IS NULL, IS NOT NULL
|
|
69
|
+
- ORDER BY, LIMIT
|
|
70
|
+
- Type mapping: TEXT, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME
|
|
71
|
+
- Reflection: get_table_names, get_columns, get_pk_constraint, has_table
|
|
72
|
+
- ORM support with DeclarativeBase
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-excel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: SQLAlchemy dialect for Excel files — use Excel as a database
|
|
5
5
|
Project-URL: Homepage, https://github.com/yeongseon/sqlalchemy-excel
|
|
6
6
|
Project-URL: Repository, https://github.com/yeongseon/sqlalchemy-excel
|
|
@@ -29,6 +29,8 @@ Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
|
29
29
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
30
30
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
31
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
32
|
+
Provides-Extra: graph
|
|
33
|
+
Requires-Dist: excel-dbapi[graph]>=0.2.0; extra == 'graph'
|
|
32
34
|
Description-Content-Type: text/markdown
|
|
33
35
|
|
|
34
36
|
<p align="left">
|
|
@@ -89,6 +91,33 @@ engine = create_engine("excel:////home/user/data.xlsx")
|
|
|
89
91
|
engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
## Remote Excel via Microsoft Graph API
|
|
95
|
+
|
|
96
|
+
Access Excel files on OneDrive/SharePoint directly:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pip install sqlalchemy-excel[graph]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from sqlalchemy import create_engine
|
|
104
|
+
from azure.identity import DefaultAzureCredential
|
|
105
|
+
|
|
106
|
+
engine = create_engine(
|
|
107
|
+
"excel+graph:///drive_id/item_id",
|
|
108
|
+
connect_args={"credential": DefaultAzureCredential()},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
with engine.connect() as conn:
|
|
112
|
+
result = conn.execute(text("SELECT * FROM Sheet1"))
|
|
113
|
+
for row in result:
|
|
114
|
+
print(row)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
URL format: `excel+graph:///drive_id/item_id` where `drive_id` and `item_id` are Microsoft Graph resource identifiers.
|
|
118
|
+
Query parameters: `?readonly=false` to enable write operations.
|
|
119
|
+
|
|
120
|
+
|
|
92
121
|
## Features
|
|
93
122
|
|
|
94
123
|
- Full SQLAlchemy 2.0 dialect
|
|
@@ -56,6 +56,33 @@ engine = create_engine("excel:////home/user/data.xlsx")
|
|
|
56
56
|
engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## Remote Excel via Microsoft Graph API
|
|
60
|
+
|
|
61
|
+
Access Excel files on OneDrive/SharePoint directly:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install sqlalchemy-excel[graph]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from sqlalchemy import create_engine
|
|
69
|
+
from azure.identity import DefaultAzureCredential
|
|
70
|
+
|
|
71
|
+
engine = create_engine(
|
|
72
|
+
"excel+graph:///drive_id/item_id",
|
|
73
|
+
connect_args={"credential": DefaultAzureCredential()},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
with engine.connect() as conn:
|
|
77
|
+
result = conn.execute(text("SELECT * FROM Sheet1"))
|
|
78
|
+
for row in result:
|
|
79
|
+
print(row)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
URL format: `excel+graph:///drive_id/item_id` where `drive_id` and `item_id` are Microsoft Graph resource identifiers.
|
|
83
|
+
Query parameters: `?readonly=false` to enable write operations.
|
|
84
|
+
|
|
85
|
+
|
|
59
86
|
## Features
|
|
60
87
|
|
|
61
88
|
- Full SQLAlchemy 2.0 dialect
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
This guide covers how to set up a development environment and contribute to sqlalchemy-excel.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
### Clone the Repository
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/yeongseon/sqlalchemy-excel.git
|
|
11
|
+
cd sqlalchemy-excel
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Set Up Virtual Environment
|
|
15
|
+
|
|
16
|
+
Create and activate a virtual environment:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Using venv (Python 3.10+)
|
|
20
|
+
python -m venv .venv
|
|
21
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
22
|
+
|
|
23
|
+
# Or using virtualenv
|
|
24
|
+
virtualenv .venv
|
|
25
|
+
source .venv/bin/activate
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Install Development Dependencies
|
|
29
|
+
|
|
30
|
+
Install the package in editable mode with development dependencies:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e ".[dev]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This installs:
|
|
37
|
+
- `sqlalchemy>=2.0`
|
|
38
|
+
- `excel-dbapi>=0.2.0`
|
|
39
|
+
- `pytest>=8.0`
|
|
40
|
+
- `pytest-cov>=4.0`
|
|
41
|
+
- `ruff>=0.4`
|
|
42
|
+
- `mypy>=1.10`
|
|
43
|
+
|
|
44
|
+
## Makefile Commands
|
|
45
|
+
|
|
46
|
+
The project includes a Makefile for common development tasks:
|
|
47
|
+
|
|
48
|
+
| Command | Description |
|
|
49
|
+
|---------|-------------|
|
|
50
|
+
| `make install` | Install the package in editable mode with dev dependencies |
|
|
51
|
+
| `make format` | Format code with ruff |
|
|
52
|
+
| `make lint` | Run linting checks with ruff and mypy |
|
|
53
|
+
| `make test` | Run tests with pytest |
|
|
54
|
+
| `make coverage` | Run tests with coverage report |
|
|
55
|
+
| `make build` | Build distribution packages |
|
|
56
|
+
| `make clean` | Remove build artifacts and cache files |
|
|
57
|
+
|
|
58
|
+
## Running Tests
|
|
59
|
+
|
|
60
|
+
Run the test suite using pytest:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Run all tests
|
|
64
|
+
pytest
|
|
65
|
+
|
|
66
|
+
# Or use make
|
|
67
|
+
make test
|
|
68
|
+
|
|
69
|
+
# Run with coverage
|
|
70
|
+
pytest --cov=sqlalchemy_excel --cov-report=html
|
|
71
|
+
make coverage
|
|
72
|
+
|
|
73
|
+
# Run specific test file
|
|
74
|
+
pytest tests/test_dialect.py
|
|
75
|
+
|
|
76
|
+
# Run specific test
|
|
77
|
+
pytest tests/test_dialect.py::test_create_engine
|
|
78
|
+
|
|
79
|
+
# Run with verbose output
|
|
80
|
+
pytest -v
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Test files are located in the `tests/` directory.
|
|
84
|
+
|
|
85
|
+
## Code Style
|
|
86
|
+
|
|
87
|
+
sqlalchemy-excel follows strict code quality standards:
|
|
88
|
+
|
|
89
|
+
### Linting and Formatting
|
|
90
|
+
|
|
91
|
+
**Ruff** is used for both linting and formatting:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Format code (auto-fix)
|
|
95
|
+
ruff format .
|
|
96
|
+
|
|
97
|
+
# Check linting (without fixing)
|
|
98
|
+
ruff check .
|
|
99
|
+
|
|
100
|
+
# Auto-fix linting issues
|
|
101
|
+
ruff check --fix .
|
|
102
|
+
|
|
103
|
+
# Or use make
|
|
104
|
+
make format # Format + auto-fix
|
|
105
|
+
make lint # Check without fixing
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Configuration is in `pyproject.toml`:
|
|
109
|
+
- Target version: Python 3.10+
|
|
110
|
+
- Line length: 88 characters
|
|
111
|
+
- Enabled rules: pycodestyle, pyflakes, isort, pep8-naming, pyupgrade, flake8-bugbear, flake8-simplify, flake8-type-checking, ruff-specific
|
|
112
|
+
|
|
113
|
+
### Type Checking
|
|
114
|
+
|
|
115
|
+
**mypy** is configured in strict mode:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
mypy src/sqlalchemy_excel
|
|
119
|
+
|
|
120
|
+
# Or as part of make lint
|
|
121
|
+
make lint
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Configuration (`pyproject.toml`):
|
|
125
|
+
- `strict = true`
|
|
126
|
+
- `warn_return_any = true`
|
|
127
|
+
- `warn_unused_configs = true`
|
|
128
|
+
|
|
129
|
+
All code must pass strict type checking before merging.
|
|
130
|
+
|
|
131
|
+
## Project Structure
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
sqlalchemy-excel/
|
|
135
|
+
├── src/sqlalchemy_excel/ # Main source code
|
|
136
|
+
│ ├── __init__.py # Package entry point
|
|
137
|
+
│ ├── dialect.py # ExcelDialect implementation
|
|
138
|
+
│ ├── compiler.py # SQL compilation (ExcelCompiler, DDLCompiler, TypeCompiler)
|
|
139
|
+
│ ├── types.py # Type mappings
|
|
140
|
+
│ └── py.typed # PEP 561 marker file
|
|
141
|
+
├── tests/ # Test suite
|
|
142
|
+
│ ├── test_dialect.py
|
|
143
|
+
│ ├── test_compiler.py
|
|
144
|
+
│ └── fixtures/ # Test data files
|
|
145
|
+
├── docs/ # Documentation
|
|
146
|
+
│ ├── USAGE.md
|
|
147
|
+
│ ├── DEVELOPMENT.md
|
|
148
|
+
│ └── ROADMAP.md
|
|
149
|
+
├── pyproject.toml # Project metadata and config
|
|
150
|
+
├── README.md # Main documentation
|
|
151
|
+
├── CHANGELOG.md # Version history
|
|
152
|
+
├── LICENSE # MIT License
|
|
153
|
+
└── Makefile # Development commands
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Development Workflow
|
|
157
|
+
|
|
158
|
+
1. **Create a feature branch**:
|
|
159
|
+
```bash
|
|
160
|
+
git checkout -b feature/my-feature
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
2. **Make changes** and add tests
|
|
164
|
+
|
|
165
|
+
3. **Format and lint**:
|
|
166
|
+
```bash
|
|
167
|
+
make format
|
|
168
|
+
make lint
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
4. **Run tests**:
|
|
172
|
+
```bash
|
|
173
|
+
make test
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
5. **Commit changes**:
|
|
177
|
+
```bash
|
|
178
|
+
git add .
|
|
179
|
+
git commit -m "feat: add new feature"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
6. **Push and create pull request**:
|
|
183
|
+
```bash
|
|
184
|
+
git push origin feature/my-feature
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Release Process
|
|
188
|
+
|
|
189
|
+
sqlalchemy-excel uses GitHub Actions for automated releases:
|
|
190
|
+
|
|
191
|
+
### 1. Update CHANGELOG.md
|
|
192
|
+
|
|
193
|
+
Document all changes in the changelog following the format:
|
|
194
|
+
|
|
195
|
+
```markdown
|
|
196
|
+
## [0.3.0] - 2024-01-15
|
|
197
|
+
|
|
198
|
+
### Added
|
|
199
|
+
- New feature X
|
|
200
|
+
- Support for Y
|
|
201
|
+
|
|
202
|
+
### Fixed
|
|
203
|
+
- Bug Z
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 2. Bump Version
|
|
207
|
+
|
|
208
|
+
Update the version in `pyproject.toml`:
|
|
209
|
+
|
|
210
|
+
```toml
|
|
211
|
+
[project]
|
|
212
|
+
version = "0.3.0"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 3. Create Git Tag
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
git add pyproject.toml CHANGELOG.md
|
|
219
|
+
git commit -m "chore: bump version to 0.3.0"
|
|
220
|
+
git tag v0.3.0
|
|
221
|
+
git push origin main
|
|
222
|
+
git push origin v0.3.0
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 4. Automated Publishing
|
|
226
|
+
|
|
227
|
+
When you push a tag (`v*`), GitHub Actions automatically:
|
|
228
|
+
1. Runs all tests and linting
|
|
229
|
+
2. Builds distribution packages (`sdist` and `wheel`)
|
|
230
|
+
3. Publishes to PyPI using **Trusted Publisher (OIDC)**
|
|
231
|
+
|
|
232
|
+
**No API token needed** — the project uses PyPI's Trusted Publisher feature with OIDC authentication configured in GitHub Actions.
|
|
233
|
+
|
|
234
|
+
### 5. Verify Release
|
|
235
|
+
|
|
236
|
+
Check that the release appears on:
|
|
237
|
+
- PyPI: https://pypi.org/project/sqlalchemy-excel/
|
|
238
|
+
- GitHub Releases: https://github.com/yeongseon/sqlalchemy-excel/releases
|
|
239
|
+
|
|
240
|
+
## Continuous Integration
|
|
241
|
+
|
|
242
|
+
The CI pipeline (`.github/workflows/ci.yml`) runs on every push and pull request:
|
|
243
|
+
|
|
244
|
+
1. **Linting**: ruff + mypy
|
|
245
|
+
2. **Testing**: pytest on Python 3.10, 3.11, 3.12, 3.13
|
|
246
|
+
3. **Coverage**: Upload to Codecov
|
|
247
|
+
|
|
248
|
+
All checks must pass before merging.
|
|
249
|
+
|
|
250
|
+
## Contributing Guidelines
|
|
251
|
+
|
|
252
|
+
- Write tests for all new features and bug fixes
|
|
253
|
+
- Maintain or improve code coverage (target: 90%+)
|
|
254
|
+
- Follow the existing code style (enforced by ruff)
|
|
255
|
+
- Add type hints for all functions (enforced by mypy strict mode)
|
|
256
|
+
- Update documentation for user-facing changes
|
|
257
|
+
- Keep commits atomic and write clear commit messages
|
|
258
|
+
|
|
259
|
+
## Getting Help
|
|
260
|
+
|
|
261
|
+
- Open an issue: https://github.com/yeongseon/sqlalchemy-excel/issues
|
|
262
|
+
- Check existing discussions and issues
|
|
263
|
+
- Review the main README and USAGE guide
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Project Roadmap
|
|
2
|
+
|
|
3
|
+
This roadmap outlines the past achievements and future plans for sqlalchemy-excel.
|
|
4
|
+
|
|
5
|
+
## Completed Features
|
|
6
|
+
|
|
7
|
+
### v0.1.0 — Initial Release
|
|
8
|
+
- ✅ SQLAlchemy 2.0 dialect implementation
|
|
9
|
+
- ✅ ORM support with `DeclarativeBase`
|
|
10
|
+
- ✅ Basic SQL operations: SELECT, INSERT, UPDATE, DELETE
|
|
11
|
+
- ✅ WHERE clauses with comparison operators
|
|
12
|
+
- ✅ ORDER BY and LIMIT support
|
|
13
|
+
- ✅ Type mapping for common SQLAlchemy types
|
|
14
|
+
- ✅ Integration with excel-dbapi driver
|
|
15
|
+
- ✅ Schema inspection (`get_table_names`, `get_columns`, `has_table`)
|
|
16
|
+
|
|
17
|
+
### v0.2.x — Dialect Rewrite and Quality Improvements
|
|
18
|
+
- ✅ Complete dialect architecture rewrite
|
|
19
|
+
- ExcelCompiler for SQL compilation
|
|
20
|
+
- DDLCompiler for CREATE/DROP TABLE
|
|
21
|
+
- TypeCompiler for type system
|
|
22
|
+
- ✅ Enhanced operator support: IN, BETWEEN, LIKE
|
|
23
|
+
- ✅ Codecov integration for coverage tracking
|
|
24
|
+
- ✅ mypy strict mode with full type safety
|
|
25
|
+
- ✅ Comprehensive test suite
|
|
26
|
+
- ✅ Improved documentation and examples
|
|
27
|
+
- ✅ GitHub Actions CI/CD pipeline
|
|
28
|
+
- ✅ PyPI Trusted Publisher (OIDC) for secure releases
|
|
29
|
+
|
|
30
|
+
## Planned Features
|
|
31
|
+
|
|
32
|
+
### High Priority
|
|
33
|
+
|
|
34
|
+
#### Remote Excel Access via Microsoft Graph API
|
|
35
|
+
- [ ] Implement `excel+graph://` URL scheme
|
|
36
|
+
- [ ] Support for OneDrive and SharePoint Excel files
|
|
37
|
+
- [ ] OAuth 2.0 authentication flow
|
|
38
|
+
- [ ] Read/write operations on cloud-stored Excel files
|
|
39
|
+
- [ ] Caching layer for remote files
|
|
40
|
+
|
|
41
|
+
**Use case**: Access and query Excel files stored in Microsoft 365 without downloading them locally.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Future API
|
|
45
|
+
engine = create_engine(
|
|
46
|
+
"excel+graph:///sites/mysite/documents/data.xlsx",
|
|
47
|
+
connect_args={
|
|
48
|
+
"tenant_id": "...",
|
|
49
|
+
"client_id": "...",
|
|
50
|
+
"client_secret": "..."
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Medium Priority
|
|
56
|
+
|
|
57
|
+
#### Advanced SQL Support
|
|
58
|
+
- [ ] **DISTINCT**: Remove duplicate rows
|
|
59
|
+
- [ ] **OFFSET**: Pagination support (currently only LIMIT works)
|
|
60
|
+
- [ ] **Aggregate functions**: COUNT, SUM, AVG, MIN, MAX
|
|
61
|
+
- [ ] **GROUP BY**: Grouping and aggregation
|
|
62
|
+
- [ ] **HAVING**: Filtering on aggregated data
|
|
63
|
+
- [ ] **Subqueries**: Nested SELECT statements
|
|
64
|
+
- [ ] **CTEs (Common Table Expressions)**: WITH clauses
|
|
65
|
+
|
|
66
|
+
**Status**: These features require significant changes to the excel-dbapi query engine. Aggregate functions are particularly complex due to Excel's storage model.
|
|
67
|
+
|
|
68
|
+
#### Multi-Table Operations
|
|
69
|
+
- [ ] **JOIN support**: INNER JOIN, LEFT JOIN, RIGHT JOIN
|
|
70
|
+
- [ ] Cross-sheet queries
|
|
71
|
+
- [ ] Foreign key awareness (metadata only, no enforcement)
|
|
72
|
+
|
|
73
|
+
**Challenge**: Excel has no native concept of relationships or joins. Implementation would require loading and joining data in memory.
|
|
74
|
+
|
|
75
|
+
#### Performance Optimization
|
|
76
|
+
- [ ] Lazy loading for large Excel files
|
|
77
|
+
- [ ] Column-level filtering (avoid loading entire rows)
|
|
78
|
+
- [ ] Query result caching
|
|
79
|
+
- [ ] Batch operation optimization
|
|
80
|
+
- [ ] Memory-efficient streaming for large datasets
|
|
81
|
+
|
|
82
|
+
**Target**: Support Excel files with 100K+ rows without excessive memory usage.
|
|
83
|
+
|
|
84
|
+
### Low Priority
|
|
85
|
+
|
|
86
|
+
#### Async Support
|
|
87
|
+
- [ ] Async dialect (`excel+aio://`)
|
|
88
|
+
- [ ] AsyncEngine and AsyncSession support
|
|
89
|
+
- [ ] Non-blocking I/O for file operations
|
|
90
|
+
|
|
91
|
+
**Note**: Requires asyncio-compatible openpyxl wrapper or alternative Excel library.
|
|
92
|
+
|
|
93
|
+
#### Additional Features
|
|
94
|
+
- [ ] Support for Excel formulas in queries
|
|
95
|
+
- [ ] Worksheet-level transactions (via temporary files)
|
|
96
|
+
- [ ] ALTER TABLE support (add/remove columns)
|
|
97
|
+
- [ ] Index simulation for faster lookups
|
|
98
|
+
- [ ] Excel template support (preserve formatting)
|
|
99
|
+
- [ ] Multiple sheet joins within same file
|
|
100
|
+
|
|
101
|
+
## Known Issues and Limitations
|
|
102
|
+
|
|
103
|
+
### Current Limitations (By Design)
|
|
104
|
+
- No transactional rollback (Excel files don't support ACID transactions)
|
|
105
|
+
- No concurrent writes (Excel file format limitations)
|
|
106
|
+
- Limited SQL feature set compared to traditional RDBMS
|
|
107
|
+
- Performance degrades with very large files (>50MB)
|
|
108
|
+
|
|
109
|
+
### Under Consideration
|
|
110
|
+
- **Alternative Excel engines**: Support for xlrd, xlwt, pyexcel in addition to openpyxl
|
|
111
|
+
- **CSV fallback**: Automatic conversion to CSV for read-only operations
|
|
112
|
+
- **SQLite hybrid mode**: Use SQLite as intermediate cache for complex queries
|
|
113
|
+
|
|
114
|
+
## Community Feedback
|
|
115
|
+
|
|
116
|
+
We welcome feedback on this roadmap! Please:
|
|
117
|
+
- 🌟 Star the repository if you find it useful
|
|
118
|
+
- 💬 Open an issue to suggest new features
|
|
119
|
+
- 🐛 Report bugs and edge cases
|
|
120
|
+
- 📝 Contribute to documentation improvements
|
|
121
|
+
- 🔀 Submit pull requests for planned features
|
|
122
|
+
|
|
123
|
+
**Priority is driven by community demand** — let us know what you need!
|
|
124
|
+
|
|
125
|
+
## Versioning Strategy
|
|
126
|
+
|
|
127
|
+
sqlalchemy-excel follows [Semantic Versioning](https://semver.org/):
|
|
128
|
+
|
|
129
|
+
- **MAJOR (1.0.0)**: Stable API, production-ready, breaking changes
|
|
130
|
+
- **MINOR (0.x.0)**: New features, backward-compatible
|
|
131
|
+
- **PATCH (0.0.x)**: Bug fixes, no new features
|
|
132
|
+
|
|
133
|
+
**Current status**: Beta (0.x.x) — API may change before 1.0.0.
|
|
134
|
+
|
|
135
|
+
## Long-Term Vision
|
|
136
|
+
|
|
137
|
+
The goal of sqlalchemy-excel is to:
|
|
138
|
+
1. Provide a **seamless SQLAlchemy experience** for Excel files
|
|
139
|
+
2. Enable **analysts and developers** to use familiar SQL tools with Excel
|
|
140
|
+
3. Bridge the gap between **ad-hoc Excel data** and **structured database workflows**
|
|
141
|
+
4. Support **cloud-native Excel** (Microsoft 365, Google Sheets in future)
|
|
142
|
+
|
|
143
|
+
**Not a goal**: Replace traditional databases for production workloads. Excel is great for prototyping, data analysis, and small-scale applications, but RDBMS should be used for critical systems.
|
|
144
|
+
|
|
145
|
+
## Contributing to the Roadmap
|
|
146
|
+
|
|
147
|
+
See [DEVELOPMENT.md](DEVELOPMENT.md) for contribution guidelines.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
**Last updated**: 2024-01 (v0.2.2)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Usage Guide
|
|
2
|
+
|
|
3
|
+
This guide covers how to use sqlalchemy-excel in your projects.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sqlalchemy-excel
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The underlying driver `excel-dbapi` is automatically installed as a dependency.
|
|
12
|
+
|
|
13
|
+
## URL Format
|
|
14
|
+
|
|
15
|
+
sqlalchemy-excel uses the `excel://` URL scheme:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from sqlalchemy import create_engine
|
|
19
|
+
|
|
20
|
+
# Relative path (relative to current working directory)
|
|
21
|
+
engine = create_engine("excel:///data.xlsx")
|
|
22
|
+
|
|
23
|
+
# Absolute path (note four slashes total: excel:// + //)
|
|
24
|
+
engine = create_engine("excel:////home/user/data.xlsx")
|
|
25
|
+
engine = create_engine("excel:////Users/alice/Documents/data.xlsx")
|
|
26
|
+
|
|
27
|
+
# With engine options (passed to connect_args)
|
|
28
|
+
engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Important**: Absolute paths require **four slashes** total (`excel:////absolute/path.xlsx`).
|
|
32
|
+
|
|
33
|
+
## Basic ORM Usage
|
|
34
|
+
|
|
35
|
+
### Define Models
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sqlalchemy import create_engine
|
|
39
|
+
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
40
|
+
|
|
41
|
+
engine = create_engine("excel:///data.xlsx")
|
|
42
|
+
|
|
43
|
+
class Base(DeclarativeBase):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
class User(Base):
|
|
47
|
+
__tablename__ = "users"
|
|
48
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
49
|
+
name: Mapped[str] = mapped_column()
|
|
50
|
+
age: Mapped[int] = mapped_column()
|
|
51
|
+
|
|
52
|
+
# Create the table (sheet) if it doesn't exist
|
|
53
|
+
Base.metadata.create_all(engine)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Use Sessions
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Insert data
|
|
60
|
+
with Session(engine) as session:
|
|
61
|
+
session.add(User(id=1, name="Alice", age=30))
|
|
62
|
+
session.add(User(id=2, name="Bob", age=25))
|
|
63
|
+
session.commit()
|
|
64
|
+
|
|
65
|
+
# Query data
|
|
66
|
+
with Session(engine) as session:
|
|
67
|
+
users = session.query(User).all()
|
|
68
|
+
for user in users:
|
|
69
|
+
print(f"{user.id}: {user.name} ({user.age})")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Core Usage
|
|
73
|
+
|
|
74
|
+
You can also use SQLAlchemy Core with `text()` queries:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from sqlalchemy import create_engine, text
|
|
78
|
+
|
|
79
|
+
engine = create_engine("excel:///data.xlsx")
|
|
80
|
+
|
|
81
|
+
with engine.connect() as conn:
|
|
82
|
+
# Execute a raw SQL query
|
|
83
|
+
result = conn.execute(text("SELECT * FROM Sheet1"))
|
|
84
|
+
for row in result:
|
|
85
|
+
print(row)
|
|
86
|
+
|
|
87
|
+
# Parameterized query (always use this for security)
|
|
88
|
+
result = conn.execute(
|
|
89
|
+
text("SELECT * FROM users WHERE name = :name"),
|
|
90
|
+
{"name": "Alice"}
|
|
91
|
+
)
|
|
92
|
+
for row in result:
|
|
93
|
+
print(row)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Query Examples
|
|
97
|
+
|
|
98
|
+
### WHERE Clause
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from sqlalchemy import select
|
|
102
|
+
|
|
103
|
+
with Session(engine) as session:
|
|
104
|
+
# Simple equality
|
|
105
|
+
user = session.query(User).filter(User.name == "Alice").first()
|
|
106
|
+
|
|
107
|
+
# Comparison operators
|
|
108
|
+
stmt = select(User).where(User.age > 25)
|
|
109
|
+
users = session.scalars(stmt).all()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### IN Operator
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
with Session(engine) as session:
|
|
116
|
+
stmt = select(User).where(User.name.in_(["Alice", "Bob", "Charlie"]))
|
|
117
|
+
users = session.scalars(stmt).all()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### BETWEEN Operator
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
with Session(engine) as session:
|
|
124
|
+
# Find users with age between 25 and 35
|
|
125
|
+
stmt = select(User).where(User.age.between(25, 35))
|
|
126
|
+
users = session.scalars(stmt).all()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### LIKE Operator
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
with Session(engine) as session:
|
|
133
|
+
# Find users whose name starts with 'A'
|
|
134
|
+
stmt = select(User).where(User.name.like("A%"))
|
|
135
|
+
users = session.scalars(stmt).all()
|
|
136
|
+
|
|
137
|
+
# Contains 'li'
|
|
138
|
+
stmt = select(User).where(User.name.like("%li%"))
|
|
139
|
+
users = session.scalars(stmt).all()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### ORDER BY and LIMIT
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
with Session(engine) as session:
|
|
146
|
+
# Order by age descending
|
|
147
|
+
stmt = select(User).order_by(User.age.desc())
|
|
148
|
+
users = session.scalars(stmt).all()
|
|
149
|
+
|
|
150
|
+
# Get top 5 oldest users
|
|
151
|
+
stmt = select(User).order_by(User.age.desc()).limit(5)
|
|
152
|
+
users = session.scalars(stmt).all()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Insert, Update, and Delete
|
|
156
|
+
|
|
157
|
+
### Insert
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
with Session(engine) as session:
|
|
161
|
+
new_user = User(id=3, name="Charlie", age=28)
|
|
162
|
+
session.add(new_user)
|
|
163
|
+
session.commit()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Update
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
with Session(engine) as session:
|
|
170
|
+
user = session.query(User).filter(User.id == 1).first()
|
|
171
|
+
if user:
|
|
172
|
+
user.name = "Ann"
|
|
173
|
+
user.age = 31
|
|
174
|
+
session.commit()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Delete
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
with Session(engine) as session:
|
|
181
|
+
user = session.query(User).filter(User.id == 2).first()
|
|
182
|
+
if user:
|
|
183
|
+
session.delete(user)
|
|
184
|
+
session.commit()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Schema Inspection
|
|
188
|
+
|
|
189
|
+
SQLAlchemy's inspector API works with Excel files:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from sqlalchemy import create_engine, inspect
|
|
193
|
+
|
|
194
|
+
engine = create_engine("excel:///data.xlsx")
|
|
195
|
+
inspector = inspect(engine)
|
|
196
|
+
|
|
197
|
+
# List all sheets (tables)
|
|
198
|
+
table_names = inspector.get_table_names()
|
|
199
|
+
print(f"Available sheets: {table_names}")
|
|
200
|
+
|
|
201
|
+
# Get columns for a specific sheet
|
|
202
|
+
columns = inspector.get_columns("users")
|
|
203
|
+
for col in columns:
|
|
204
|
+
print(f"{col['name']}: {col['type']}")
|
|
205
|
+
|
|
206
|
+
# Check if a sheet exists
|
|
207
|
+
if inspector.has_table("users"):
|
|
208
|
+
print("The 'users' sheet exists")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Type Mapping
|
|
212
|
+
|
|
213
|
+
sqlalchemy-excel maps SQLAlchemy types to Excel storage types:
|
|
214
|
+
|
|
215
|
+
| SQLAlchemy Type | Excel Storage | Notes |
|
|
216
|
+
|-----------------|---------------|-------|
|
|
217
|
+
| `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types → TEXT |
|
|
218
|
+
| `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types → INTEGER |
|
|
219
|
+
| `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types → FLOAT |
|
|
220
|
+
| `Boolean` | BOOLEAN | Stored as boolean |
|
|
221
|
+
| `Date` | DATE | Date without time |
|
|
222
|
+
| `DateTime`, `TIMESTAMP` | DATETIME | Date with time |
|
|
223
|
+
| `Time` | TEXT | Stored as text |
|
|
224
|
+
| `Uuid` | TEXT | Stored as text |
|
|
225
|
+
|
|
226
|
+
**Unsupported types**: BLOB, BINARY, JSON, ARRAY (will raise `CompileError`)
|
|
227
|
+
|
|
228
|
+
## Limitations
|
|
229
|
+
|
|
230
|
+
sqlalchemy-excel has some limitations due to the nature of Excel as a database:
|
|
231
|
+
|
|
232
|
+
- **No JOIN operations**: Single-table queries only
|
|
233
|
+
- **No GROUP BY, HAVING, DISTINCT**: Aggregations not supported
|
|
234
|
+
- **No OFFSET**: Only LIMIT is supported
|
|
235
|
+
- **No subqueries or CTEs**: Simple queries only
|
|
236
|
+
- **No aggregate functions**: COUNT, SUM, AVG, etc. not available
|
|
237
|
+
- **No ALTER TABLE**: Cannot modify table structure after creation
|
|
238
|
+
- **No foreign keys or indexes**: Excel has no concept of these
|
|
239
|
+
- **No concurrent writes**: Use a single-writer model
|
|
240
|
+
- **Rollback is a no-op**: `Session.rollback()` does nothing — Excel files don't support transactional rollback
|
|
241
|
+
|
|
242
|
+
## Security
|
|
243
|
+
|
|
244
|
+
**Always use parameterized queries** to prevent SQL injection:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
# ✅ GOOD: Parameterized query
|
|
248
|
+
with engine.connect() as conn:
|
|
249
|
+
result = conn.execute(
|
|
250
|
+
text("SELECT * FROM users WHERE name = :name"),
|
|
251
|
+
{"name": user_input}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# ❌ BAD: String interpolation (vulnerable to SQL injection)
|
|
255
|
+
with engine.connect() as conn:
|
|
256
|
+
result = conn.execute(
|
|
257
|
+
text(f"SELECT * FROM users WHERE name = '{user_input}'")
|
|
258
|
+
)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
SQLAlchemy ORM queries are automatically parameterized and safe:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
# ✅ SAFE: ORM automatically parameterizes
|
|
265
|
+
with Session(engine) as session:
|
|
266
|
+
user = session.query(User).filter(User.name == user_input).first()
|
|
267
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#1DB954"/>
|
|
5
|
+
<stop offset="100%" stop-color="#148A40"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- Base (same as excel-dbapi) -->
|
|
10
|
+
<rect x="8" y="8" width="112" height="112" rx="26" fill="url(#bg)"/>
|
|
11
|
+
|
|
12
|
+
<!-- Left: bold SA angle bracket -->
|
|
13
|
+
<path d="M52 30 L26 64 L52 98" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"/>
|
|
14
|
+
|
|
15
|
+
<!-- Right: 2×1 grid cells (spreadsheet) -->
|
|
16
|
+
<rect x="64" y="36" width="36" height="24" rx="5" fill="#fff" opacity="0.92"/>
|
|
17
|
+
<rect x="64" y="66" width="36" height="24" rx="5" fill="#fff" opacity="0.68"/>
|
|
18
|
+
|
|
19
|
+
<!-- Bridge: connector -->
|
|
20
|
+
<line x1="55" y1="64" x2="61" y2="64" stroke="#fff" stroke-width="4" stroke-linecap="round" opacity="0.60"/>
|
|
21
|
+
</svg>
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-excel"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "SQLAlchemy dialect for Excel files — use Excel as a database"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
36
|
+
graph = ["excel-dbapi[graph]>=0.2.0"]
|
|
36
37
|
dev = [
|
|
37
38
|
"pytest>=8.0",
|
|
38
39
|
"pytest-cov>=4.0",
|
|
@@ -42,6 +43,7 @@ dev = [
|
|
|
42
43
|
|
|
43
44
|
[project.entry-points."sqlalchemy.dialects"]
|
|
44
45
|
excel = "sqlalchemy_excel.dialect:ExcelDialect"
|
|
46
|
+
"excel.graph" = "sqlalchemy_excel.dialect:ExcelGraphDialect"
|
|
45
47
|
|
|
46
48
|
[project.urls]
|
|
47
49
|
Homepage = "https://github.com/yeongseon/sqlalchemy-excel"
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from .dialect import ExcelDialect
|
|
5
|
+
from .dialect import ExcelDialect, ExcelGraphDialect
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.3.1"
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"ExcelDialect",
|
|
11
|
+
"ExcelGraphDialect",
|
|
11
12
|
"__version__",
|
|
12
13
|
]
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
7
|
+
from urllib.parse import unquote as _url_unquote
|
|
7
8
|
|
|
8
9
|
from sqlalchemy import event, pool
|
|
9
10
|
from sqlalchemy.engine import default
|
|
@@ -230,3 +231,63 @@ class ExcelDialect( # type: ignore[misc] # pyright: ignore[reportIncompatibleM
|
|
|
230
231
|
def do_close(self, dbapi_connection: Any) -> None:
|
|
231
232
|
"""Close the underlying excel-dbapi connection."""
|
|
232
233
|
dbapi_connection.close()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ExcelGraphDialect(ExcelDialect): # type: ignore[misc,unused-ignore]
|
|
237
|
+
"""SQLAlchemy dialect for remote Excel files via Microsoft Graph API.
|
|
238
|
+
|
|
239
|
+
Connection URLs::
|
|
240
|
+
|
|
241
|
+
# With drive_id and item_id
|
|
242
|
+
excel+graph:///drive_id/item_id
|
|
243
|
+
|
|
244
|
+
# With query parameters
|
|
245
|
+
excel+graph:///drive_id/item_id?readonly=false
|
|
246
|
+
|
|
247
|
+
Credentials must be passed via ``connect_args``::
|
|
248
|
+
|
|
249
|
+
engine = create_engine(
|
|
250
|
+
"excel+graph:///drive_id/item_id",
|
|
251
|
+
connect_args={"credential": DefaultAzureCredential()},
|
|
252
|
+
)
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
driver: str = "graph"
|
|
256
|
+
supports_statement_cache: bool = False
|
|
257
|
+
|
|
258
|
+
def create_connect_args(self, url: URL) -> ConnectArgsType:
|
|
259
|
+
"""Translate an excel+graph:// URL to excel-dbapi connect() arguments.
|
|
260
|
+
|
|
261
|
+
URL format: excel+graph:///drive_id/item_id
|
|
262
|
+
Maps to DSN: msgraph://drives/{drive_id}/items/{item_id}
|
|
263
|
+
"""
|
|
264
|
+
database = url.database
|
|
265
|
+
if not database:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"No drive/item path in URL. Use excel+graph:///drive_id/item_id"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
parts = database.strip("/").split("/")
|
|
271
|
+
if len(parts) != 2:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"Expected excel+graph:///drive_id/item_id (got {len(parts)} path segments: {database!r})"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
drive_id = _url_unquote(parts[0])
|
|
277
|
+
item_id = _url_unquote(parts[1])
|
|
278
|
+
dsn = f"msgraph://drives/{drive_id}/items/{item_id}"
|
|
279
|
+
|
|
280
|
+
kwargs = {
|
|
281
|
+
"file_path": dsn,
|
|
282
|
+
"engine": "graph",
|
|
283
|
+
"autocommit": True,
|
|
284
|
+
"create": False,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
query = dict(url.query)
|
|
288
|
+
if "readonly" in query:
|
|
289
|
+
raw = query.pop("readonly")
|
|
290
|
+
val = raw[0] if isinstance(raw, tuple) else raw
|
|
291
|
+
kwargs["readonly"] = str(val).lower() in ("true", "1", "yes")
|
|
292
|
+
|
|
293
|
+
return ([], kwargs)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Tests for ExcelGraphDialect — URL parsing and Graph API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
from sqlalchemy import create_engine, text
|
|
8
|
+
from sqlalchemy.dialects import registry
|
|
9
|
+
from sqlalchemy.engine import make_url
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Mock transport (minimal Graph API stub)
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _graph_handler(request: httpx.Request) -> httpx.Response:
|
|
17
|
+
"""Stateless mock handler for read-only Graph API tests."""
|
|
18
|
+
path = request.url.path
|
|
19
|
+
method = request.method
|
|
20
|
+
|
|
21
|
+
if path.endswith("/createSession"):
|
|
22
|
+
return httpx.Response(201, json={"id": "sess-graph-test"})
|
|
23
|
+
if path.endswith("/closeSession"):
|
|
24
|
+
return httpx.Response(204)
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
path.endswith("/worksheets") or "/worksheets?" in str(request.url)
|
|
28
|
+
) and method == "GET":
|
|
29
|
+
return httpx.Response(
|
|
30
|
+
200,
|
|
31
|
+
json={"value": [{"id": "ws-sheet1", "name": "Sheet1"}]},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if "usedRange" in path and method == "GET":
|
|
35
|
+
return httpx.Response(
|
|
36
|
+
200,
|
|
37
|
+
json={
|
|
38
|
+
"values": [
|
|
39
|
+
["id", "name", "value"],
|
|
40
|
+
[1, "Alice", 100],
|
|
41
|
+
[2, "Bob", 200],
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return httpx.Response(404)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# URL Parsing Tests
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestGraphURLParsing:
|
|
55
|
+
def test_url_components(self):
|
|
56
|
+
url = make_url("excel+graph:///drv-abc/itm-xyz")
|
|
57
|
+
assert url.get_backend_name() == "excel"
|
|
58
|
+
assert url.get_driver_name() == "graph"
|
|
59
|
+
assert url.database == "drv-abc/itm-xyz"
|
|
60
|
+
|
|
61
|
+
def test_url_with_host_ignored(self):
|
|
62
|
+
"""Host part (tenant_id) is allowed but unused."""
|
|
63
|
+
url = make_url("excel+graph://my-tenant/drv-abc/itm-xyz")
|
|
64
|
+
assert url.host == "my-tenant"
|
|
65
|
+
assert url.database == "drv-abc/itm-xyz"
|
|
66
|
+
|
|
67
|
+
def test_create_connect_args_basic(self):
|
|
68
|
+
dialect = registry.load("excel.graph")()
|
|
69
|
+
url = make_url("excel+graph:///drv-abc/itm-xyz")
|
|
70
|
+
args, kwargs = dialect.create_connect_args(url)
|
|
71
|
+
assert args == []
|
|
72
|
+
assert kwargs["file_path"] == "msgraph://drives/drv-abc/items/itm-xyz"
|
|
73
|
+
assert kwargs["engine"] == "graph"
|
|
74
|
+
assert kwargs["autocommit"] is True
|
|
75
|
+
assert kwargs["create"] is False
|
|
76
|
+
|
|
77
|
+
def test_create_connect_args_url_decoding(self):
|
|
78
|
+
"""Drive/item IDs with percent-encoded chars should be decoded."""
|
|
79
|
+
dialect = registry.load("excel.graph")()
|
|
80
|
+
url = make_url("excel+graph:///b%21abc/itm%2D123")
|
|
81
|
+
_, kwargs = dialect.create_connect_args(url)
|
|
82
|
+
assert kwargs["file_path"] == "msgraph://drives/b!abc/items/itm-123"
|
|
83
|
+
|
|
84
|
+
def test_create_connect_args_readonly_false(self):
|
|
85
|
+
dialect = registry.load("excel.graph")()
|
|
86
|
+
url = make_url("excel+graph:///drv/itm?readonly=false")
|
|
87
|
+
_, kwargs = dialect.create_connect_args(url)
|
|
88
|
+
assert kwargs.get("readonly") is False
|
|
89
|
+
|
|
90
|
+
def test_create_connect_args_readonly_true(self):
|
|
91
|
+
dialect = registry.load("excel.graph")()
|
|
92
|
+
url = make_url("excel+graph:///drv/itm?readonly=true")
|
|
93
|
+
_, kwargs = dialect.create_connect_args(url)
|
|
94
|
+
assert kwargs.get("readonly") is True
|
|
95
|
+
|
|
96
|
+
def test_empty_path_raises(self):
|
|
97
|
+
dialect = registry.load("excel.graph")()
|
|
98
|
+
url = make_url("excel+graph://")
|
|
99
|
+
with pytest.raises(ValueError, match="No drive/item path"):
|
|
100
|
+
_ = dialect.create_connect_args(url)
|
|
101
|
+
|
|
102
|
+
def test_single_segment_raises(self):
|
|
103
|
+
dialect = registry.load("excel.graph")()
|
|
104
|
+
url = make_url("excel+graph:///only-one")
|
|
105
|
+
with pytest.raises(ValueError, match="path segments"):
|
|
106
|
+
_ = dialect.create_connect_args(url)
|
|
107
|
+
|
|
108
|
+
def test_three_segments_raises(self):
|
|
109
|
+
dialect = registry.load("excel.graph")()
|
|
110
|
+
url = make_url("excel+graph:///a/b/c")
|
|
111
|
+
with pytest.raises(ValueError, match="path segments"):
|
|
112
|
+
_ = dialect.create_connect_args(url)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Dialect Feature Flags
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestGraphDialectFlags:
|
|
121
|
+
def test_driver(self):
|
|
122
|
+
d = registry.load("excel.graph")()
|
|
123
|
+
assert d.driver == "graph"
|
|
124
|
+
|
|
125
|
+
def test_name(self):
|
|
126
|
+
d = registry.load("excel.graph")()
|
|
127
|
+
assert d.name == "excel"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Integration: SELECT via mock transport
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestGraphDialectIntegration:
|
|
136
|
+
def test_select_via_engine(self):
|
|
137
|
+
"""Full round-trip: create_engine → connect → SELECT."""
|
|
138
|
+
transport = httpx.MockTransport(_graph_handler)
|
|
139
|
+
engine = create_engine(
|
|
140
|
+
"excel+graph:///drv-test/itm-test",
|
|
141
|
+
connect_args={
|
|
142
|
+
"credential": "test-token",
|
|
143
|
+
"transport": transport,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
with engine.connect() as conn:
|
|
147
|
+
result = conn.execute(text("SELECT * FROM Sheet1"))
|
|
148
|
+
rows = result.fetchall()
|
|
149
|
+
assert len(rows) == 2
|
|
150
|
+
assert rows[0] == (1, "Alice", 100)
|
|
151
|
+
engine.dispose()
|
|
152
|
+
|
|
153
|
+
def test_select_with_where(self):
|
|
154
|
+
transport = httpx.MockTransport(_graph_handler)
|
|
155
|
+
engine = create_engine(
|
|
156
|
+
"excel+graph:///drv-test/itm-test",
|
|
157
|
+
connect_args={
|
|
158
|
+
"credential": "test-token",
|
|
159
|
+
"transport": transport,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
with engine.connect() as conn:
|
|
163
|
+
result = conn.execute(
|
|
164
|
+
text("SELECT name FROM Sheet1 WHERE id = :id"),
|
|
165
|
+
{"id": 1},
|
|
166
|
+
)
|
|
167
|
+
rows = result.fetchall()
|
|
168
|
+
assert len(rows) == 1
|
|
169
|
+
assert rows[0] == ("Alice",)
|
|
170
|
+
engine.dispose()
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.0 (2026-04-12)
|
|
4
|
-
|
|
5
|
-
- Initial release
|
|
6
|
-
- SQLAlchemy 2.0 dialect for Excel files
|
|
7
|
-
- PEP 249 DB-API 2.0 driver via excel-dbapi
|
|
8
|
-
- SQL support: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
|
|
9
|
-
- WHERE clause with AND/OR, comparison operators, IS NULL, IS NOT NULL
|
|
10
|
-
- ORDER BY, LIMIT
|
|
11
|
-
- Type mapping: TEXT, INTEGER, FLOAT, BOOLEAN, DATE, DATETIME
|
|
12
|
-
- Reflection: get_table_names, get_columns, get_pk_constraint, has_table
|
|
13
|
-
- ORM support with DeclarativeBase
|
sqlalchemy_excel-0.2.2/logo.svg
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
|
2
|
-
<!-- SQL code bracket left -->
|
|
3
|
-
<path d="M10 38 L22 28 L22 34 L16 40 L16 64 L22 70 L22 76 L10 66 L10 38Z" fill="#CE4926" opacity="0.85" />
|
|
4
|
-
<!-- SQL code bracket right -->
|
|
5
|
-
<path d="M58 38 L46 28 L46 34 L52 40 L52 64 L46 70 L46 76 L58 66 L58 38Z" fill="#CE4926" opacity="0.85" />
|
|
6
|
-
<!-- SQL text -->
|
|
7
|
-
<text x="34" y="58" font-family="monospace" font-size="14" font-weight="bold" fill="#CE4926" text-anchor="middle">SA</text>
|
|
8
|
-
<!-- Arrow / bridge -->
|
|
9
|
-
<line x1="62" y1="52" x2="72" y2="52" stroke="#666" stroke-width="2" stroke-linecap="round" />
|
|
10
|
-
<polygon points="72,48 80,52 72,56" fill="#666" />
|
|
11
|
-
<!-- Excel spreadsheet -->
|
|
12
|
-
<rect x="82" y="16" width="40" height="56" rx="4" fill="#217346" />
|
|
13
|
-
<rect x="86" y="22" width="32" height="44" rx="2" fill="#fff" />
|
|
14
|
-
<!-- Grid lines -->
|
|
15
|
-
<line x1="86" y1="33" x2="118" y2="33" stroke="#217346" stroke-width="1" opacity="0.3" />
|
|
16
|
-
<line x1="86" y1="44" x2="118" y2="44" stroke="#217346" stroke-width="1" opacity="0.3" />
|
|
17
|
-
<line x1="86" y1="55" x2="118" y2="55" stroke="#217346" stroke-width="1" opacity="0.3" />
|
|
18
|
-
<line x1="97" y1="22" x2="97" y2="66" stroke="#217346" stroke-width="1" opacity="0.3" />
|
|
19
|
-
<line x1="107" y1="22" x2="107" y2="66" stroke="#217346" stroke-width="1" opacity="0.3" />
|
|
20
|
-
<!-- Dialect label -->
|
|
21
|
-
<rect x="14" y="88" width="100" height="24" rx="12" fill="#3572A5" />
|
|
22
|
-
<text x="64" y="104" font-family="sans-serif" font-size="11" font-weight="bold" fill="#fff" text-anchor="middle">dialect</text>
|
|
23
|
-
</svg>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_excel-0.2.2 → sqlalchemy_excel-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
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
|