sqlalchemy-excel 0.1.1__tar.gz → 0.2.0__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.1.1 → sqlalchemy_excel-0.2.0}/.github/workflows/ci.yml +9 -2
- sqlalchemy_excel-0.2.0/PKG-INFO +236 -0
- sqlalchemy_excel-0.2.0/README.md +204 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/pyproject.toml +2 -2
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/__init__.py +1 -1
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/compiler.py +44 -14
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/ddl.py +13 -2
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/dialect.py +33 -22
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/reflection.py +4 -4
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_dml.py +55 -0
- sqlalchemy_excel-0.1.1/PKG-INFO +0 -96
- sqlalchemy_excel-0.1.1/README.md +0 -64
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/.github/workflows/publish-pypi.yml +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/.gitignore +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/CHANGELOG.md +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/LICENSE +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/src/sqlalchemy_excel/types.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/conftest.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_compiler.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_ddl.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_dialect.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_orm.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_reflection.py +0 -0
- {sqlalchemy_excel-0.1.1 → sqlalchemy_excel-0.2.0}/tests/test_types.py +0 -0
|
@@ -34,6 +34,13 @@ jobs:
|
|
|
34
34
|
run: |
|
|
35
35
|
mypy --strict src/
|
|
36
36
|
|
|
37
|
-
- name:
|
|
37
|
+
- name: Run tests with coverage
|
|
38
38
|
run: |
|
|
39
|
-
pytest -v --
|
|
39
|
+
pytest tests/ -v --cov=sqlalchemy_excel --cov-report=xml --cov-report=term-missing
|
|
40
|
+
|
|
41
|
+
- name: Upload coverage to Codecov
|
|
42
|
+
if: matrix.python-version == '3.12'
|
|
43
|
+
uses: codecov/codecov-action@v5
|
|
44
|
+
with:
|
|
45
|
+
files: ./coverage.xml
|
|
46
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-excel
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: SQLAlchemy dialect for Excel files — use Excel as a database
|
|
5
|
+
Project-URL: Homepage, https://github.com/yeongseon/sqlalchemy-excel
|
|
6
|
+
Project-URL: Repository, https://github.com/yeongseon/sqlalchemy-excel
|
|
7
|
+
Project-URL: Issues, https://github.com/yeongseon/sqlalchemy-excel/issues
|
|
8
|
+
Author: Yeongseon Choe
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: database,dialect,excel,openpyxl,sqlalchemy
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: excel-dbapi>=0.2.0
|
|
25
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# sqlalchemy-excel
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
[](https://codecov.io/gh/yeongseon/sqlalchemy-excel)
|
|
37
|
+
[](https://pypi.org/project/sqlalchemy-excel/)
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
SQLAlchemy dialect for Excel files — use Excel as a database.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from sqlalchemy import create_engine, Column, Integer, String
|
|
45
|
+
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
46
|
+
|
|
47
|
+
engine = create_engine("excel:///data.xlsx")
|
|
48
|
+
|
|
49
|
+
class Base(DeclarativeBase):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
class User(Base):
|
|
53
|
+
__tablename__ = "Sheet1"
|
|
54
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
55
|
+
name: Mapped[str] = mapped_column()
|
|
56
|
+
|
|
57
|
+
Base.metadata.create_all(engine)
|
|
58
|
+
|
|
59
|
+
with Session(engine) as session:
|
|
60
|
+
session.add(User(id=1, name="Alice"))
|
|
61
|
+
session.commit()
|
|
62
|
+
|
|
63
|
+
with Session(engine) as session:
|
|
64
|
+
users = session.query(User).all()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install sqlalchemy-excel
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`excel-dbapi` is automatically installed as a dependency.
|
|
74
|
+
|
|
75
|
+
## URL Format
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Relative path
|
|
79
|
+
engine = create_engine("excel:///data.xlsx")
|
|
80
|
+
|
|
81
|
+
# Absolute path (note four slashes)
|
|
82
|
+
engine = create_engine("excel:////home/user/data.xlsx")
|
|
83
|
+
|
|
84
|
+
# With engine options
|
|
85
|
+
engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- Full SQLAlchemy 2.0 dialect
|
|
91
|
+
- PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
|
|
92
|
+
- SELECT with WHERE, ORDER BY, LIMIT
|
|
93
|
+
- INSERT, UPDATE, DELETE
|
|
94
|
+
- CREATE TABLE / DROP TABLE with metadata tracking
|
|
95
|
+
- IN, BETWEEN, LIKE operators in WHERE clauses
|
|
96
|
+
- ORM support with `DeclarativeBase`
|
|
97
|
+
- Schema inspection (`get_table_names`, `get_columns`, `has_table`)
|
|
98
|
+
- Type mapping: String, Integer, Float, Boolean, Date, DateTime
|
|
99
|
+
|
|
100
|
+
## Type Mapping
|
|
101
|
+
|
|
102
|
+
| SQLAlchemy Type | Excel Storage | Notes |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types map to TEXT |
|
|
105
|
+
| `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types map to INTEGER |
|
|
106
|
+
| `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types map to FLOAT |
|
|
107
|
+
| `Boolean` | BOOLEAN | |
|
|
108
|
+
| `Date` | DATE | |
|
|
109
|
+
| `DateTime`, `TIMESTAMP` | DATETIME | |
|
|
110
|
+
| `Time` | TEXT | Stored as text |
|
|
111
|
+
| `Uuid` | TEXT | Stored as text |
|
|
112
|
+
|
|
113
|
+
> BLOB, BINARY, JSON, and ARRAY types are not supported and will raise `CompileError`.
|
|
114
|
+
|
|
115
|
+
## ORM Examples
|
|
116
|
+
|
|
117
|
+
### Define a Model
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from sqlalchemy import create_engine
|
|
121
|
+
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
122
|
+
|
|
123
|
+
engine = create_engine("excel:///data.xlsx")
|
|
124
|
+
|
|
125
|
+
class Base(DeclarativeBase):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
class User(Base):
|
|
129
|
+
__tablename__ = "users"
|
|
130
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
131
|
+
name: Mapped[str] = mapped_column()
|
|
132
|
+
age: Mapped[int] = mapped_column()
|
|
133
|
+
|
|
134
|
+
Base.metadata.create_all(engine)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Insert
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
with Session(engine) as session:
|
|
141
|
+
session.add(User(id=1, name="Alice", age=30))
|
|
142
|
+
session.add(User(id=2, name="Bob", age=25))
|
|
143
|
+
session.commit()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Query with Filters
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from sqlalchemy import select
|
|
150
|
+
|
|
151
|
+
with Session(engine) as session:
|
|
152
|
+
# Basic query
|
|
153
|
+
users = session.query(User).all()
|
|
154
|
+
|
|
155
|
+
# WHERE clause
|
|
156
|
+
user = session.query(User).filter(User.name == "Alice").first()
|
|
157
|
+
|
|
158
|
+
# IN operator
|
|
159
|
+
stmt = select(User).where(User.name.in_(["Alice", "Bob"]))
|
|
160
|
+
users = session.scalars(stmt).all()
|
|
161
|
+
|
|
162
|
+
# BETWEEN operator
|
|
163
|
+
stmt = select(User).where(User.age.between(25, 35))
|
|
164
|
+
users = session.scalars(stmt).all()
|
|
165
|
+
|
|
166
|
+
# LIKE operator
|
|
167
|
+
stmt = select(User).where(User.name.like("A%"))
|
|
168
|
+
users = session.scalars(stmt).all()
|
|
169
|
+
|
|
170
|
+
# ORDER BY + LIMIT
|
|
171
|
+
stmt = select(User).order_by(User.age.desc()).limit(5)
|
|
172
|
+
users = session.scalars(stmt).all()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Update and Delete
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
with Session(engine) as session:
|
|
179
|
+
user = session.query(User).filter(User.id == 1).first()
|
|
180
|
+
if user:
|
|
181
|
+
user.name = "Ann"
|
|
182
|
+
session.commit()
|
|
183
|
+
|
|
184
|
+
with Session(engine) as session:
|
|
185
|
+
user = session.query(User).filter(User.id == 2).first()
|
|
186
|
+
if user:
|
|
187
|
+
session.delete(user)
|
|
188
|
+
session.commit()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Core Usage
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from sqlalchemy import create_engine, text
|
|
195
|
+
|
|
196
|
+
engine = create_engine("excel:///data.xlsx")
|
|
197
|
+
|
|
198
|
+
with engine.connect() as conn:
|
|
199
|
+
result = conn.execute(text("SELECT * FROM Sheet1"))
|
|
200
|
+
for row in result:
|
|
201
|
+
print(row)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Schema Inspection
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from sqlalchemy import create_engine, inspect
|
|
208
|
+
|
|
209
|
+
engine = create_engine("excel:///data.xlsx")
|
|
210
|
+
inspector = inspect(engine)
|
|
211
|
+
|
|
212
|
+
# List all sheets (tables)
|
|
213
|
+
print(inspector.get_table_names())
|
|
214
|
+
|
|
215
|
+
# Get column info
|
|
216
|
+
print(inspector.get_columns("Sheet1"))
|
|
217
|
+
|
|
218
|
+
# Check if a sheet exists
|
|
219
|
+
print(inspector.has_table("Sheet1"))
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Limitations
|
|
223
|
+
|
|
224
|
+
- No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
|
|
225
|
+
- No subqueries, CTEs, or aggregate functions
|
|
226
|
+
- No ALTER TABLE, foreign keys, or indexes
|
|
227
|
+
- Single-table operations only
|
|
228
|
+
- No concurrent writes — use a single-writer model
|
|
229
|
+
|
|
230
|
+
## Related Projects
|
|
231
|
+
|
|
232
|
+
- [excel-dbapi](https://github.com/yeongseon/excel-dbapi) — The underlying PEP 249 DB-API 2.0 driver for Excel files.
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# sqlalchemy-excel
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://codecov.io/gh/yeongseon/sqlalchemy-excel)
|
|
5
|
+
[](https://pypi.org/project/sqlalchemy-excel/)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
SQLAlchemy dialect for Excel files — use Excel as a database.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from sqlalchemy import create_engine, Column, Integer, String
|
|
13
|
+
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
14
|
+
|
|
15
|
+
engine = create_engine("excel:///data.xlsx")
|
|
16
|
+
|
|
17
|
+
class Base(DeclarativeBase):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class User(Base):
|
|
21
|
+
__tablename__ = "Sheet1"
|
|
22
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
23
|
+
name: Mapped[str] = mapped_column()
|
|
24
|
+
|
|
25
|
+
Base.metadata.create_all(engine)
|
|
26
|
+
|
|
27
|
+
with Session(engine) as session:
|
|
28
|
+
session.add(User(id=1, name="Alice"))
|
|
29
|
+
session.commit()
|
|
30
|
+
|
|
31
|
+
with Session(engine) as session:
|
|
32
|
+
users = session.query(User).all()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sqlalchemy-excel
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`excel-dbapi` is automatically installed as a dependency.
|
|
42
|
+
|
|
43
|
+
## URL Format
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Relative path
|
|
47
|
+
engine = create_engine("excel:///data.xlsx")
|
|
48
|
+
|
|
49
|
+
# Absolute path (note four slashes)
|
|
50
|
+
engine = create_engine("excel:////home/user/data.xlsx")
|
|
51
|
+
|
|
52
|
+
# With engine options
|
|
53
|
+
engine = create_engine("excel:///data.xlsx", connect_args={"engine": "openpyxl"})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- Full SQLAlchemy 2.0 dialect
|
|
59
|
+
- PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
|
|
60
|
+
- SELECT with WHERE, ORDER BY, LIMIT
|
|
61
|
+
- INSERT, UPDATE, DELETE
|
|
62
|
+
- CREATE TABLE / DROP TABLE with metadata tracking
|
|
63
|
+
- IN, BETWEEN, LIKE operators in WHERE clauses
|
|
64
|
+
- ORM support with `DeclarativeBase`
|
|
65
|
+
- Schema inspection (`get_table_names`, `get_columns`, `has_table`)
|
|
66
|
+
- Type mapping: String, Integer, Float, Boolean, Date, DateTime
|
|
67
|
+
|
|
68
|
+
## Type Mapping
|
|
69
|
+
|
|
70
|
+
| SQLAlchemy Type | Excel Storage | Notes |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `String`, `Text`, `VARCHAR`, `CHAR` | TEXT | All string types map to TEXT |
|
|
73
|
+
| `Integer`, `SmallInteger`, `BigInteger` | INTEGER | All integer types map to INTEGER |
|
|
74
|
+
| `Float`, `Numeric`, `Decimal` | FLOAT | All numeric types map to FLOAT |
|
|
75
|
+
| `Boolean` | BOOLEAN | |
|
|
76
|
+
| `Date` | DATE | |
|
|
77
|
+
| `DateTime`, `TIMESTAMP` | DATETIME | |
|
|
78
|
+
| `Time` | TEXT | Stored as text |
|
|
79
|
+
| `Uuid` | TEXT | Stored as text |
|
|
80
|
+
|
|
81
|
+
> BLOB, BINARY, JSON, and ARRAY types are not supported and will raise `CompileError`.
|
|
82
|
+
|
|
83
|
+
## ORM Examples
|
|
84
|
+
|
|
85
|
+
### Define a Model
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from sqlalchemy import create_engine
|
|
89
|
+
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
90
|
+
|
|
91
|
+
engine = create_engine("excel:///data.xlsx")
|
|
92
|
+
|
|
93
|
+
class Base(DeclarativeBase):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
class User(Base):
|
|
97
|
+
__tablename__ = "users"
|
|
98
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
99
|
+
name: Mapped[str] = mapped_column()
|
|
100
|
+
age: Mapped[int] = mapped_column()
|
|
101
|
+
|
|
102
|
+
Base.metadata.create_all(engine)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Insert
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
with Session(engine) as session:
|
|
109
|
+
session.add(User(id=1, name="Alice", age=30))
|
|
110
|
+
session.add(User(id=2, name="Bob", age=25))
|
|
111
|
+
session.commit()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Query with Filters
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from sqlalchemy import select
|
|
118
|
+
|
|
119
|
+
with Session(engine) as session:
|
|
120
|
+
# Basic query
|
|
121
|
+
users = session.query(User).all()
|
|
122
|
+
|
|
123
|
+
# WHERE clause
|
|
124
|
+
user = session.query(User).filter(User.name == "Alice").first()
|
|
125
|
+
|
|
126
|
+
# IN operator
|
|
127
|
+
stmt = select(User).where(User.name.in_(["Alice", "Bob"]))
|
|
128
|
+
users = session.scalars(stmt).all()
|
|
129
|
+
|
|
130
|
+
# BETWEEN operator
|
|
131
|
+
stmt = select(User).where(User.age.between(25, 35))
|
|
132
|
+
users = session.scalars(stmt).all()
|
|
133
|
+
|
|
134
|
+
# LIKE operator
|
|
135
|
+
stmt = select(User).where(User.name.like("A%"))
|
|
136
|
+
users = session.scalars(stmt).all()
|
|
137
|
+
|
|
138
|
+
# ORDER BY + LIMIT
|
|
139
|
+
stmt = select(User).order_by(User.age.desc()).limit(5)
|
|
140
|
+
users = session.scalars(stmt).all()
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Update and Delete
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
with Session(engine) as session:
|
|
147
|
+
user = session.query(User).filter(User.id == 1).first()
|
|
148
|
+
if user:
|
|
149
|
+
user.name = "Ann"
|
|
150
|
+
session.commit()
|
|
151
|
+
|
|
152
|
+
with Session(engine) as session:
|
|
153
|
+
user = session.query(User).filter(User.id == 2).first()
|
|
154
|
+
if user:
|
|
155
|
+
session.delete(user)
|
|
156
|
+
session.commit()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Core Usage
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from sqlalchemy import create_engine, text
|
|
163
|
+
|
|
164
|
+
engine = create_engine("excel:///data.xlsx")
|
|
165
|
+
|
|
166
|
+
with engine.connect() as conn:
|
|
167
|
+
result = conn.execute(text("SELECT * FROM Sheet1"))
|
|
168
|
+
for row in result:
|
|
169
|
+
print(row)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Schema Inspection
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from sqlalchemy import create_engine, inspect
|
|
176
|
+
|
|
177
|
+
engine = create_engine("excel:///data.xlsx")
|
|
178
|
+
inspector = inspect(engine)
|
|
179
|
+
|
|
180
|
+
# List all sheets (tables)
|
|
181
|
+
print(inspector.get_table_names())
|
|
182
|
+
|
|
183
|
+
# Get column info
|
|
184
|
+
print(inspector.get_columns("Sheet1"))
|
|
185
|
+
|
|
186
|
+
# Check if a sheet exists
|
|
187
|
+
print(inspector.has_table("Sheet1"))
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Limitations
|
|
191
|
+
|
|
192
|
+
- No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
|
|
193
|
+
- No subqueries, CTEs, or aggregate functions
|
|
194
|
+
- No ALTER TABLE, foreign keys, or indexes
|
|
195
|
+
- Single-table operations only
|
|
196
|
+
- No concurrent writes — use a single-writer model
|
|
197
|
+
|
|
198
|
+
## Related Projects
|
|
199
|
+
|
|
200
|
+
- [excel-dbapi](https://github.com/yeongseon/excel-dbapi) — The underlying PEP 249 DB-API 2.0 driver for Excel files.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-excel"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "SQLAlchemy dialect for Excel files — use Excel as a database"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -29,7 +29,7 @@ classifiers = [
|
|
|
29
29
|
|
|
30
30
|
dependencies = [
|
|
31
31
|
"sqlalchemy>=2.0",
|
|
32
|
-
"excel-dbapi>=0.
|
|
32
|
+
"excel-dbapi>=0.2.0",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
@@ -22,11 +22,14 @@ So we override the identifier preparer to never use table prefixes.
|
|
|
22
22
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
-
from typing import Any,
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
26
26
|
|
|
27
27
|
from sqlalchemy import exc
|
|
28
28
|
from sqlalchemy.sql import compiler, elements
|
|
29
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import MutableMapping
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
class ExcelIdentifierPreparer(compiler.IdentifierPreparer):
|
|
32
35
|
"""Identifier preparer that never quotes identifiers.
|
|
@@ -35,10 +38,9 @@ class ExcelIdentifierPreparer(compiler.IdentifierPreparer):
|
|
|
35
38
|
or table prefixes.
|
|
36
39
|
"""
|
|
37
40
|
|
|
38
|
-
reserved_words: ClassVar[set[str]] = set()
|
|
39
|
-
|
|
40
41
|
def __init__(self, dialect: Any) -> None:
|
|
41
42
|
super().__init__(dialect, initial_quote="", final_quote="")
|
|
43
|
+
self.reserved_words = set()
|
|
42
44
|
|
|
43
45
|
def quote_identifier(self, value: str) -> str:
|
|
44
46
|
return value
|
|
@@ -55,7 +57,9 @@ class ExcelCompiler(compiler.SQLCompiler):
|
|
|
55
57
|
column: Any,
|
|
56
58
|
add_to_result_map: Any = None,
|
|
57
59
|
include_table: bool = True,
|
|
58
|
-
|
|
60
|
+
result_map_targets: tuple[Any, ...] = (),
|
|
61
|
+
ambiguous_table_name_map: MutableMapping[str, str] | None = None,
|
|
62
|
+
**kwargs: Any,
|
|
59
63
|
) -> str:
|
|
60
64
|
"""Override to never include table prefix in column references.
|
|
61
65
|
|
|
@@ -67,7 +71,9 @@ class ExcelCompiler(compiler.SQLCompiler):
|
|
|
67
71
|
column,
|
|
68
72
|
add_to_result_map=add_to_result_map,
|
|
69
73
|
include_table=False,
|
|
70
|
-
|
|
74
|
+
result_map_targets=result_map_targets,
|
|
75
|
+
ambiguous_table_name_map=ambiguous_table_name_map,
|
|
76
|
+
**kwargs,
|
|
71
77
|
)
|
|
72
78
|
|
|
73
79
|
def visit_label(
|
|
@@ -104,11 +110,13 @@ class ExcelCompiler(compiler.SQLCompiler):
|
|
|
104
110
|
)
|
|
105
111
|
|
|
106
112
|
# Emit the column WITHOUT "AS <label>"
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
return str(
|
|
114
|
+
label.element._compiler_dispatch(
|
|
115
|
+
self,
|
|
116
|
+
within_columns_clause=True,
|
|
117
|
+
within_label_clause=True,
|
|
118
|
+
**kw,
|
|
119
|
+
)
|
|
112
120
|
)
|
|
113
121
|
|
|
114
122
|
if render_label_as_label is label:
|
|
@@ -118,11 +126,19 @@ class ExcelCompiler(compiler.SQLCompiler):
|
|
|
118
126
|
labelname = label.name
|
|
119
127
|
return self.preparer.format_label(label, labelname)
|
|
120
128
|
|
|
121
|
-
return
|
|
129
|
+
return str(
|
|
130
|
+
label.element._compiler_dispatch(self, within_columns_clause=False, **kw)
|
|
131
|
+
)
|
|
122
132
|
|
|
123
133
|
# ── Unsupported feature guards ─────────────────────────
|
|
124
134
|
|
|
125
|
-
def visit_join(
|
|
135
|
+
def visit_join(
|
|
136
|
+
self,
|
|
137
|
+
join: Any,
|
|
138
|
+
asfrom: Any = False,
|
|
139
|
+
from_linter: Any = None,
|
|
140
|
+
**kwargs: Any,
|
|
141
|
+
) -> str:
|
|
126
142
|
raise exc.CompileError("Excel dialect does not support JOIN")
|
|
127
143
|
|
|
128
144
|
def group_by_clause(self, select: Any, **kw: Any) -> str:
|
|
@@ -141,7 +157,17 @@ class ExcelCompiler(compiler.SQLCompiler):
|
|
|
141
157
|
raise exc.CompileError("Excel dialect does not support OFFSET")
|
|
142
158
|
return text
|
|
143
159
|
|
|
144
|
-
def visit_cte(
|
|
160
|
+
def visit_cte(
|
|
161
|
+
self,
|
|
162
|
+
cte: Any,
|
|
163
|
+
asfrom: bool = False,
|
|
164
|
+
ashint: bool = False,
|
|
165
|
+
fromhints: dict[Any, str] | None = None,
|
|
166
|
+
visiting_cte: Any = None,
|
|
167
|
+
from_linter: Any = None,
|
|
168
|
+
cte_opts: Any = None,
|
|
169
|
+
**kwargs: Any,
|
|
170
|
+
) -> str | None:
|
|
145
171
|
raise exc.CompileError("Excel dialect does not support CTEs")
|
|
146
172
|
|
|
147
173
|
def visit_subquery(self, subquery: Any, **kw: Any) -> str:
|
|
@@ -155,5 +181,9 @@ class ExcelCompiler(compiler.SQLCompiler):
|
|
|
155
181
|
) -> str:
|
|
156
182
|
raise exc.CompileError("Excel dialect does not support RETURNING")
|
|
157
183
|
|
|
158
|
-
def for_update_clause(
|
|
184
|
+
def for_update_clause(
|
|
185
|
+
self,
|
|
186
|
+
select: Any,
|
|
187
|
+
**kw: Any,
|
|
188
|
+
) -> Literal[" FOR UPDATE"]:
|
|
159
189
|
raise exc.CompileError("Excel dialect does not support SELECT ... FOR UPDATE")
|
|
@@ -38,7 +38,13 @@ class ExcelDDLCompiler(compiler.DDLCompiler):
|
|
|
38
38
|
table_name = self.preparer.format_table(table)
|
|
39
39
|
return f"DROP TABLE {table_name}"
|
|
40
40
|
|
|
41
|
-
def visit_create_index(
|
|
41
|
+
def visit_create_index(
|
|
42
|
+
self,
|
|
43
|
+
create: Any,
|
|
44
|
+
include_schema: Any = False,
|
|
45
|
+
include_table_schema: Any = True,
|
|
46
|
+
**kw: Any,
|
|
47
|
+
) -> str:
|
|
42
48
|
raise exc.CompileError("Excel dialect does not support CREATE INDEX")
|
|
43
49
|
|
|
44
50
|
def visit_drop_index(self, drop: Any, **kw: Any) -> str:
|
|
@@ -50,7 +56,12 @@ class ExcelDDLCompiler(compiler.DDLCompiler):
|
|
|
50
56
|
def visit_drop_constraint(self, drop: Any, **kw: Any) -> str:
|
|
51
57
|
raise exc.CompileError("Excel dialect does not support constraints")
|
|
52
58
|
|
|
53
|
-
def visit_create_sequence(
|
|
59
|
+
def visit_create_sequence(
|
|
60
|
+
self,
|
|
61
|
+
create: Any,
|
|
62
|
+
prefix: Any = None,
|
|
63
|
+
**kw: Any,
|
|
64
|
+
) -> str:
|
|
54
65
|
raise exc.CompileError("Excel dialect does not support sequences")
|
|
55
66
|
|
|
56
67
|
def visit_drop_sequence(self, drop: Any, **kw: Any) -> str:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
-
from typing import TYPE_CHECKING, Any,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
7
7
|
|
|
8
8
|
from sqlalchemy import event, pool
|
|
9
9
|
from sqlalchemy.engine import default
|
|
@@ -17,6 +17,7 @@ from .types import ExcelTypeCompiler
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from sqlalchemy.engine import URL
|
|
19
19
|
from sqlalchemy.engine.interfaces import ConnectArgsType
|
|
20
|
+
from sqlalchemy.sql.compiler import IdentifierPreparer
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _after_create(
|
|
@@ -67,7 +68,10 @@ event.listen(Table, "after_create", _after_create)
|
|
|
67
68
|
event.listen(Table, "after_drop", _after_drop)
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
class ExcelDialect(
|
|
71
|
+
class ExcelDialect( # type: ignore[misc] # pyright: ignore[reportIncompatibleMethodOverride]
|
|
72
|
+
ExcelInspectionMixin,
|
|
73
|
+
default.DefaultDialect,
|
|
74
|
+
):
|
|
71
75
|
"""SQLAlchemy dialect for Excel files via excel-dbapi.
|
|
72
76
|
|
|
73
77
|
Connection URLs::
|
|
@@ -81,30 +85,32 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
|
|
|
81
85
|
|
|
82
86
|
"""
|
|
83
87
|
|
|
84
|
-
name:
|
|
85
|
-
driver:
|
|
86
|
-
default_paramstyle:
|
|
88
|
+
name: str = "excel"
|
|
89
|
+
driver: str = "dbapi"
|
|
90
|
+
default_paramstyle: str = "qmark"
|
|
87
91
|
|
|
88
92
|
# ── Feature flags ──────────────────────────────────────
|
|
89
|
-
supports_alter:
|
|
90
|
-
supports_sequences:
|
|
91
|
-
supports_schemas:
|
|
92
|
-
supports_views:
|
|
93
|
-
supports_native_boolean:
|
|
94
|
-
supports_native_decimal:
|
|
95
|
-
supports_statement_cache:
|
|
96
|
-
supports_default_values:
|
|
97
|
-
supports_default_metavalue:
|
|
98
|
-
supports_empty_insert:
|
|
99
|
-
supports_multivalues_insert:
|
|
100
|
-
postfetch_lastrowid:
|
|
101
|
-
insertmanyvalues_implicit_sentinel:
|
|
93
|
+
supports_alter: bool = False
|
|
94
|
+
supports_sequences: bool = False
|
|
95
|
+
supports_schemas: bool = False
|
|
96
|
+
supports_views: bool = False
|
|
97
|
+
supports_native_boolean: bool = True
|
|
98
|
+
supports_native_decimal: bool = False
|
|
99
|
+
supports_statement_cache: bool = False
|
|
100
|
+
supports_default_values: bool = False
|
|
101
|
+
supports_default_metavalue: bool = False
|
|
102
|
+
supports_empty_insert: bool = False
|
|
103
|
+
supports_multivalues_insert: bool = False
|
|
104
|
+
postfetch_lastrowid: bool = False
|
|
105
|
+
insertmanyvalues_implicit_sentinel: Any = None
|
|
102
106
|
|
|
103
107
|
# ── Compiler classes ──────────────────────────────────
|
|
104
108
|
statement_compiler = ExcelCompiler
|
|
105
109
|
ddl_compiler = ExcelDDLCompiler
|
|
106
110
|
type_compiler_cls = ExcelTypeCompiler
|
|
107
|
-
preparer =
|
|
111
|
+
preparer: type[IdentifierPreparer] = cast(
|
|
112
|
+
"type[IdentifierPreparer]", ExcelIdentifierPreparer
|
|
113
|
+
)
|
|
108
114
|
|
|
109
115
|
@classmethod
|
|
110
116
|
def import_dbapi(cls) -> Any:
|
|
@@ -140,7 +146,12 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
|
|
|
140
146
|
if "engine" in query:
|
|
141
147
|
kwargs["engine"] = query.pop("engine")
|
|
142
148
|
if "autocommit" in query:
|
|
143
|
-
|
|
149
|
+
autocommit = query.pop("autocommit")
|
|
150
|
+
if isinstance(autocommit, tuple):
|
|
151
|
+
autocommit_text = autocommit[0] if autocommit else ""
|
|
152
|
+
else:
|
|
153
|
+
autocommit_text = autocommit
|
|
154
|
+
kwargs["autocommit"] = autocommit_text.lower() in (
|
|
144
155
|
"true",
|
|
145
156
|
"1",
|
|
146
157
|
"yes",
|
|
@@ -180,7 +191,7 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
|
|
|
180
191
|
"""Excel connections don't have network-level disconnects."""
|
|
181
192
|
return False
|
|
182
193
|
|
|
183
|
-
def get_default_isolation_level(self, dbapi_conn: Any) ->
|
|
194
|
+
def get_default_isolation_level(self, dbapi_conn: Any) -> Literal["SERIALIZABLE"]:
|
|
184
195
|
return "SERIALIZABLE"
|
|
185
196
|
|
|
186
197
|
def _check_unicode_returns(
|
|
@@ -202,7 +213,7 @@ class ExcelDialect(ExcelInspectionMixin, default.DefaultDialect): # type: ignor
|
|
|
202
213
|
import excel_dbapi
|
|
203
214
|
|
|
204
215
|
raw_conn = connection.connection.dbapi_connection
|
|
205
|
-
return excel_dbapi.has_table(raw_conn, table_name)
|
|
216
|
+
return cast("bool", excel_dbapi.has_table(raw_conn, table_name))
|
|
206
217
|
|
|
207
218
|
def do_begin(self, dbapi_connection: Any) -> None:
|
|
208
219
|
"""No-op: excel-dbapi doesn't have explicit BEGIN."""
|
|
@@ -6,11 +6,11 @@ tables (worksheets), columns, and primary keys from an Excel file.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, cast
|
|
10
10
|
|
|
11
11
|
from sqlalchemy import types as sa_types
|
|
12
12
|
|
|
13
|
-
_TYPE_MAP: dict[str, type[sa_types.TypeEngine]] = {
|
|
13
|
+
_TYPE_MAP: dict[str, type[sa_types.TypeEngine[Any]]] = {
|
|
14
14
|
"TEXT": sa_types.String,
|
|
15
15
|
"INTEGER": sa_types.Integer,
|
|
16
16
|
"FLOAT": sa_types.Float,
|
|
@@ -20,7 +20,7 @@ _TYPE_MAP: dict[str, type[sa_types.TypeEngine]] = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _sa_type_from_name(type_name: str) -> sa_types.TypeEngine:
|
|
23
|
+
def _sa_type_from_name(type_name: str) -> sa_types.TypeEngine[Any]:
|
|
24
24
|
"""Convert an excel-dbapi type name to a SQLAlchemy type instance."""
|
|
25
25
|
cls = _TYPE_MAP.get(type_name.upper(), sa_types.String)
|
|
26
26
|
return cls()
|
|
@@ -43,7 +43,7 @@ class ExcelInspectionMixin:
|
|
|
43
43
|
import excel_dbapi
|
|
44
44
|
|
|
45
45
|
raw_conn = connection.connection.dbapi_connection
|
|
46
|
-
return excel_dbapi.list_tables(raw_conn, include_meta=False)
|
|
46
|
+
return cast("list[str]", excel_dbapi.list_tables(raw_conn, include_meta=False))
|
|
47
47
|
|
|
48
48
|
def get_view_names(
|
|
49
49
|
self,
|
|
@@ -134,6 +134,61 @@ class TestSelect:
|
|
|
134
134
|
# Each row should have 2 columns
|
|
135
135
|
assert len(rows[0]) == 2
|
|
136
136
|
|
|
137
|
+
def test_select_where_in(self, populated_engine, users_table):
|
|
138
|
+
with populated_engine.connect() as conn:
|
|
139
|
+
self._seed(conn, users_table)
|
|
140
|
+
stmt = select(users_table).where(
|
|
141
|
+
users_table.c.name.in_(["Alice", "Charlie"])
|
|
142
|
+
)
|
|
143
|
+
result = conn.execute(stmt)
|
|
144
|
+
rows = result.fetchall()
|
|
145
|
+
assert len(rows) == 2
|
|
146
|
+
names = {row[1] for row in rows}
|
|
147
|
+
assert names == {"Alice", "Charlie"}
|
|
148
|
+
|
|
149
|
+
def test_select_where_between(self, populated_engine, users_table):
|
|
150
|
+
with populated_engine.connect() as conn:
|
|
151
|
+
self._seed(conn, users_table)
|
|
152
|
+
stmt = select(users_table).where(users_table.c.age.between(26, 31))
|
|
153
|
+
result = conn.execute(stmt)
|
|
154
|
+
rows = result.fetchall()
|
|
155
|
+
assert len(rows) == 1
|
|
156
|
+
assert rows[0][1] == "Alice"
|
|
157
|
+
|
|
158
|
+
def test_select_where_like(self, populated_engine, users_table):
|
|
159
|
+
with populated_engine.connect() as conn:
|
|
160
|
+
self._seed(conn, users_table)
|
|
161
|
+
stmt = select(users_table).where(users_table.c.name.like("A%"))
|
|
162
|
+
result = conn.execute(stmt)
|
|
163
|
+
rows = result.fetchall()
|
|
164
|
+
assert len(rows) == 1
|
|
165
|
+
assert rows[0][1] == "Alice"
|
|
166
|
+
|
|
167
|
+
def test_select_where_like_contains(self, populated_engine, users_table):
|
|
168
|
+
with populated_engine.connect() as conn:
|
|
169
|
+
self._seed(conn, users_table)
|
|
170
|
+
stmt = select(users_table).where(users_table.c.name.like("%ob%"))
|
|
171
|
+
result = conn.execute(stmt)
|
|
172
|
+
rows = result.fetchall()
|
|
173
|
+
assert len(rows) == 1
|
|
174
|
+
assert rows[0][1] == "Bob"
|
|
175
|
+
|
|
176
|
+
def test_select_where_in_with_order_limit(self, populated_engine, users_table):
|
|
177
|
+
with populated_engine.connect() as conn:
|
|
178
|
+
self._seed(conn, users_table)
|
|
179
|
+
stmt = (
|
|
180
|
+
select(users_table)
|
|
181
|
+
.where(users_table.c.name.in_(["Alice", "Bob", "Charlie"]))
|
|
182
|
+
.order_by(users_table.c.age.desc())
|
|
183
|
+
.limit(2)
|
|
184
|
+
)
|
|
185
|
+
result = conn.execute(stmt)
|
|
186
|
+
rows = result.fetchall()
|
|
187
|
+
assert len(rows) == 2
|
|
188
|
+
# Charlie(35), Alice(30) — desc by age, limit 2
|
|
189
|
+
assert rows[0][1] == "Charlie"
|
|
190
|
+
assert rows[1][1] == "Alice"
|
|
191
|
+
|
|
137
192
|
|
|
138
193
|
class TestUpdate:
|
|
139
194
|
"""Test UPDATE operations."""
|
sqlalchemy_excel-0.1.1/PKG-INFO
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqlalchemy-excel
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: SQLAlchemy dialect for Excel files — use Excel as a database
|
|
5
|
-
Project-URL: Homepage, https://github.com/yeongseon/sqlalchemy-excel
|
|
6
|
-
Project-URL: Repository, https://github.com/yeongseon/sqlalchemy-excel
|
|
7
|
-
Project-URL: Issues, https://github.com/yeongseon/sqlalchemy-excel/issues
|
|
8
|
-
Author: Yeongseon Choe
|
|
9
|
-
License: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: database,dialect,excel,openpyxl,sqlalchemy
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Topic :: Database
|
|
21
|
-
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
22
|
-
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: excel-dbapi>=0.1.1
|
|
25
|
-
Requires-Dist: sqlalchemy>=2.0
|
|
26
|
-
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
-
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
29
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
-
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
31
|
-
Description-Content-Type: text/markdown
|
|
32
|
-
|
|
33
|
-
# sqlalchemy-excel
|
|
34
|
-
|
|
35
|
-
SQLAlchemy dialect for Excel files — use Excel as a database.
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
from sqlalchemy import create_engine, Column, Integer, String
|
|
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__ = "Sheet1"
|
|
48
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
49
|
-
name: Mapped[str] = mapped_column()
|
|
50
|
-
|
|
51
|
-
Base.metadata.create_all(engine)
|
|
52
|
-
|
|
53
|
-
with Session(engine) as session:
|
|
54
|
-
session.add(User(id=1, name="Alice"))
|
|
55
|
-
session.commit()
|
|
56
|
-
|
|
57
|
-
with Session(engine) as session:
|
|
58
|
-
users = session.query(User).all()
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Installation
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
pip install sqlalchemy-excel
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## URL Format
|
|
68
|
-
|
|
69
|
-
```python
|
|
70
|
-
# Relative path
|
|
71
|
-
engine = create_engine("excel:///data.xlsx")
|
|
72
|
-
|
|
73
|
-
# Absolute path (note four slashes)
|
|
74
|
-
engine = create_engine("excel:////home/user/data.xlsx")
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Features
|
|
78
|
-
|
|
79
|
-
- Full SQLAlchemy 2.0 dialect
|
|
80
|
-
- PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
|
|
81
|
-
- SELECT with WHERE, ORDER BY, LIMIT
|
|
82
|
-
- INSERT, UPDATE, DELETE
|
|
83
|
-
- CREATE TABLE / DROP TABLE
|
|
84
|
-
- ORM support with `DeclarativeBase`
|
|
85
|
-
- Type mapping: String, Integer, Float, Boolean, Date, DateTime
|
|
86
|
-
|
|
87
|
-
## Limitations
|
|
88
|
-
|
|
89
|
-
- No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
|
|
90
|
-
- No subqueries, CTEs, or aggregate functions
|
|
91
|
-
- No ALTER TABLE, foreign keys, or indexes
|
|
92
|
-
- Single-table operations only
|
|
93
|
-
|
|
94
|
-
## License
|
|
95
|
-
|
|
96
|
-
MIT
|
sqlalchemy_excel-0.1.1/README.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# sqlalchemy-excel
|
|
2
|
-
|
|
3
|
-
SQLAlchemy dialect for Excel files — use Excel as a database.
|
|
4
|
-
|
|
5
|
-
```python
|
|
6
|
-
from sqlalchemy import create_engine, Column, Integer, String
|
|
7
|
-
from sqlalchemy.orm import DeclarativeBase, Session, Mapped, mapped_column
|
|
8
|
-
|
|
9
|
-
engine = create_engine("excel:///data.xlsx")
|
|
10
|
-
|
|
11
|
-
class Base(DeclarativeBase):
|
|
12
|
-
pass
|
|
13
|
-
|
|
14
|
-
class User(Base):
|
|
15
|
-
__tablename__ = "Sheet1"
|
|
16
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
17
|
-
name: Mapped[str] = mapped_column()
|
|
18
|
-
|
|
19
|
-
Base.metadata.create_all(engine)
|
|
20
|
-
|
|
21
|
-
with Session(engine) as session:
|
|
22
|
-
session.add(User(id=1, name="Alice"))
|
|
23
|
-
session.commit()
|
|
24
|
-
|
|
25
|
-
with Session(engine) as session:
|
|
26
|
-
users = session.query(User).all()
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Installation
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pip install sqlalchemy-excel
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## URL Format
|
|
36
|
-
|
|
37
|
-
```python
|
|
38
|
-
# Relative path
|
|
39
|
-
engine = create_engine("excel:///data.xlsx")
|
|
40
|
-
|
|
41
|
-
# Absolute path (note four slashes)
|
|
42
|
-
engine = create_engine("excel:////home/user/data.xlsx")
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Features
|
|
46
|
-
|
|
47
|
-
- Full SQLAlchemy 2.0 dialect
|
|
48
|
-
- PEP 249 DB-API 2.0 compliant driver ([excel-dbapi](https://github.com/yeongseon/excel-dbapi))
|
|
49
|
-
- SELECT with WHERE, ORDER BY, LIMIT
|
|
50
|
-
- INSERT, UPDATE, DELETE
|
|
51
|
-
- CREATE TABLE / DROP TABLE
|
|
52
|
-
- ORM support with `DeclarativeBase`
|
|
53
|
-
- Type mapping: String, Integer, Float, Boolean, Date, DateTime
|
|
54
|
-
|
|
55
|
-
## Limitations
|
|
56
|
-
|
|
57
|
-
- No JOIN, GROUP BY, HAVING, DISTINCT, OFFSET
|
|
58
|
-
- No subqueries, CTEs, or aggregate functions
|
|
59
|
-
- No ALTER TABLE, foreign keys, or indexes
|
|
60
|
-
- Single-table operations only
|
|
61
|
-
|
|
62
|
-
## License
|
|
63
|
-
|
|
64
|
-
MIT
|
|
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
|