sqlalchemy-excel 0.1.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.
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ pip install -e ".[dev]"
27
+
28
+ - name: Lint
29
+ run: |
30
+ ruff check .
31
+ ruff format --check .
32
+
33
+ - name: Type check
34
+ run: |
35
+ mypy --strict src/
36
+
37
+ - name: Test
38
+ run: |
39
+ pytest -v --tb=short
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install build tools
22
+ run: pip install build
23
+
24
+ - name: Build package
25
+ run: python -m build
26
+
27
+ - name: Publish to PyPI
28
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,37 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+
23
+ # Testing / coverage
24
+ .pytest_cache/
25
+ .coverage
26
+ htmlcov/
27
+ .mypy_cache/
28
+ .ruff_cache/
29
+ .benchmarks/
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Test artifacts
36
+ *.xlsx
37
+ !tests/fixtures/*.xlsx
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yeongseon Choe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
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
@@ -0,0 +1,64 @@
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
@@ -0,0 +1,106 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sqlalchemy-excel"
7
+ version = "0.1.1"
8
+ description = "SQLAlchemy dialect for Excel files — use Excel as a database"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Yeongseon Choe"},
14
+ ]
15
+ keywords = ["sqlalchemy", "excel", "dialect", "database", "openpyxl"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Database",
26
+ "Topic :: Office/Business :: Financial :: Spreadsheet",
27
+ "Typing :: Typed",
28
+ ]
29
+
30
+ dependencies = [
31
+ "sqlalchemy>=2.0",
32
+ "excel-dbapi>=0.1.1",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "pytest-cov>=4.0",
39
+ "ruff>=0.4",
40
+ "mypy>=1.10",
41
+ ]
42
+
43
+ [project.entry-points."sqlalchemy.dialects"]
44
+ excel = "sqlalchemy_excel.dialect:ExcelDialect"
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/yeongseon/sqlalchemy-excel"
48
+ Repository = "https://github.com/yeongseon/sqlalchemy-excel"
49
+ Issues = "https://github.com/yeongseon/sqlalchemy-excel/issues"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/sqlalchemy_excel"]
53
+
54
+ [tool.ruff]
55
+ target-version = "py310"
56
+ src = ["src"]
57
+ line-length = 88
58
+
59
+ [tool.ruff.lint]
60
+ select = [
61
+ "E", # pycodestyle errors
62
+ "W", # pycodestyle warnings
63
+ "F", # pyflakes
64
+ "I", # isort
65
+ "N", # pep8-naming
66
+ "UP", # pyupgrade
67
+ "B", # flake8-bugbear
68
+ "SIM", # flake8-simplify
69
+ "TCH", # flake8-type-checking
70
+ "RUF", # ruff-specific
71
+ ]
72
+ ignore = ["E501"]
73
+
74
+ [tool.ruff.lint.per-file-ignores]
75
+ "src/sqlalchemy_excel/types.py" = ["N802"] # SQLAlchemy visit_TYPE convention
76
+ "src/sqlalchemy_excel/compiler.py" = ["RUF005"] # Match SQLAlchemy's tuple concat pattern
77
+
78
+ [tool.ruff.lint.isort]
79
+ known-first-party = ["sqlalchemy_excel"]
80
+
81
+ [tool.mypy]
82
+ python_version = "3.10"
83
+ strict = true
84
+ warn_return_any = true
85
+ warn_unused_configs = true
86
+
87
+ [[tool.mypy.overrides]]
88
+ module = "excel_dbapi.*"
89
+ ignore_missing_imports = true
90
+
91
+ [tool.pytest.ini_options]
92
+ testpaths = ["tests"]
93
+ addopts = "-ra -q"
94
+ pythonpath = ["src"]
95
+
96
+ [tool.coverage.run]
97
+ source = ["sqlalchemy_excel"]
98
+ branch = true
99
+
100
+ [tool.coverage.report]
101
+ exclude_lines = [
102
+ "pragma: no cover",
103
+ "if TYPE_CHECKING:",
104
+ "if __name__",
105
+ "@overload",
106
+ ]
@@ -0,0 +1,12 @@
1
+ """sqlalchemy-excel — SQLAlchemy dialect for Excel files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .dialect import ExcelDialect
6
+
7
+ __version__ = "0.1.1"
8
+
9
+ __all__ = [
10
+ "ExcelDialect",
11
+ "__version__",
12
+ ]
@@ -0,0 +1,159 @@
1
+ """SQL compiler for the Excel dialect.
2
+
3
+ Compiles SQLAlchemy expression trees into the SQL subset that
4
+ excel-dbapi's parser understands:
5
+
6
+ Supported:
7
+ SELECT columns FROM table [WHERE ...] [ORDER BY col [ASC|DESC]] [LIMIT n]
8
+ INSERT INTO table (cols) VALUES (vals)
9
+ UPDATE table SET col=val [WHERE ...]
10
+ DELETE FROM table [WHERE ...]
11
+
12
+ Rejected (raises CompileError):
13
+ JOIN, GROUP BY, HAVING, DISTINCT, OFFSET, subqueries,
14
+ CTEs, aggregate functions, window functions, RETURNING
15
+
16
+ excel-dbapi's parser uses unquoted, unprefixed column names:
17
+ SELECT id, name FROM users (correct)
18
+ SELECT users.id, users.name FROM users (WRONG — parser rejects)
19
+
20
+ So we override the identifier preparer to never use table prefixes.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, ClassVar
26
+
27
+ from sqlalchemy import exc
28
+ from sqlalchemy.sql import compiler, elements
29
+
30
+
31
+ class ExcelIdentifierPreparer(compiler.IdentifierPreparer):
32
+ """Identifier preparer that never quotes identifiers.
33
+
34
+ excel-dbapi's parser expects bare column names without quotes
35
+ or table prefixes.
36
+ """
37
+
38
+ reserved_words: ClassVar[set[str]] = set()
39
+
40
+ def __init__(self, dialect: Any) -> None:
41
+ super().__init__(dialect, initial_quote="", final_quote="")
42
+
43
+ def quote_identifier(self, value: str) -> str:
44
+ return value
45
+
46
+ def quote(self, ident: str, force: Any = None) -> str:
47
+ return ident
48
+
49
+
50
+ class ExcelCompiler(compiler.SQLCompiler):
51
+ """Compiles SQLAlchemy SQL expressions for excel-dbapi."""
52
+
53
+ def visit_column(
54
+ self,
55
+ column: Any,
56
+ add_to_result_map: Any = None,
57
+ include_table: bool = True,
58
+ **kw: Any,
59
+ ) -> str:
60
+ """Override to never include table prefix in column references.
61
+
62
+ excel-dbapi expects: SELECT id, name FROM users
63
+ Not: SELECT users.id, users.name FROM users
64
+ """
65
+ # Force include_table=False to avoid table.column notation
66
+ return super().visit_column(
67
+ column,
68
+ add_to_result_map=add_to_result_map,
69
+ include_table=False,
70
+ **kw,
71
+ )
72
+
73
+ def visit_label(
74
+ self,
75
+ label: Any,
76
+ add_to_result_map: Any = None,
77
+ within_label_clause: bool = False,
78
+ within_columns_clause: bool = False,
79
+ render_label_as_label: Any = None,
80
+ result_map_targets: Any = (),
81
+ **kw: Any,
82
+ ) -> str:
83
+ """Override to never emit AS <label> in SELECT columns.
84
+
85
+ excel-dbapi's parser does not understand column aliases.
86
+ We still register the result map so SQLAlchemy can map
87
+ result columns back to ORM attributes by position.
88
+ """
89
+ render_label_with_as = within_columns_clause and not within_label_clause
90
+
91
+ if render_label_with_as:
92
+ # Compute the label name for the result map
93
+ if isinstance(label.name, elements._truncated_label):
94
+ labelname = self._truncated_identifier("colident", label.name)
95
+ else:
96
+ labelname = label.name
97
+
98
+ if add_to_result_map is not None:
99
+ add_to_result_map(
100
+ labelname,
101
+ label.name,
102
+ (label, labelname) + label._alt_names + result_map_targets,
103
+ label.type,
104
+ )
105
+
106
+ # Emit the column WITHOUT "AS <label>"
107
+ return label.element._compiler_dispatch(
108
+ self,
109
+ within_columns_clause=True,
110
+ within_label_clause=True,
111
+ **kw,
112
+ )
113
+
114
+ if render_label_as_label is label:
115
+ if isinstance(label.name, elements._truncated_label):
116
+ labelname = self._truncated_identifier("colident", label.name)
117
+ else:
118
+ labelname = label.name
119
+ return self.preparer.format_label(label, labelname)
120
+
121
+ return label.element._compiler_dispatch(self, within_columns_clause=False, **kw)
122
+
123
+ # ── Unsupported feature guards ─────────────────────────
124
+
125
+ def visit_join(self, join: Any, **kw: Any) -> str:
126
+ raise exc.CompileError("Excel dialect does not support JOIN")
127
+
128
+ def group_by_clause(self, select: Any, **kw: Any) -> str:
129
+ if select._group_by_clauses:
130
+ raise exc.CompileError("Excel dialect does not support GROUP BY")
131
+ return ""
132
+
133
+ def having_clause(self, select: Any, **kw: Any) -> str:
134
+ raise exc.CompileError("Excel dialect does not support HAVING")
135
+
136
+ def limit_clause(self, select: Any, **kw: Any) -> str:
137
+ text = ""
138
+ if select._limit_clause is not None:
139
+ text += " LIMIT " + self.process(select._limit_clause, **kw)
140
+ if select._offset_clause is not None:
141
+ raise exc.CompileError("Excel dialect does not support OFFSET")
142
+ return text
143
+
144
+ def visit_cte(self, cte: Any, **kw: Any) -> str:
145
+ raise exc.CompileError("Excel dialect does not support CTEs")
146
+
147
+ def visit_subquery(self, subquery: Any, **kw: Any) -> str:
148
+ raise exc.CompileError("Excel dialect does not support subqueries")
149
+
150
+ def returning_clause(
151
+ self,
152
+ stmt: Any,
153
+ returning_cols: Any,
154
+ **kw: Any,
155
+ ) -> str:
156
+ raise exc.CompileError("Excel dialect does not support RETURNING")
157
+
158
+ def for_update_clause(self, select: Any, **kw: Any) -> str:
159
+ raise exc.CompileError("Excel dialect does not support SELECT ... FOR UPDATE")
@@ -0,0 +1,57 @@
1
+ """DDL compiler for the Excel dialect.
2
+
3
+ CREATE TABLE → creates a worksheet and writes column metadata.
4
+ DROP TABLE → deletes the worksheet and removes metadata.
5
+ ALTER TABLE → rejected (not supported).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from sqlalchemy import exc
13
+ from sqlalchemy.sql import compiler
14
+
15
+
16
+ class ExcelDDLCompiler(compiler.DDLCompiler):
17
+ """Compiles DDL statements for excel-dbapi."""
18
+
19
+ def visit_create_table(self, create: Any, **kw: Any) -> str:
20
+ """Compile CREATE TABLE into SQL that excel-dbapi's parser accepts.
21
+
22
+ Format: CREATE TABLE name (col1 TYPE, col2 TYPE, ...)
23
+ """
24
+ table = create.element
25
+ table_name = self.preparer.format_table(table)
26
+
27
+ columns = []
28
+ for col in table.columns:
29
+ col_name = self.preparer.format_column(col)
30
+ col_type = self.dialect.type_compiler.process(col.type)
31
+ columns.append(f"{col_name} {col_type}")
32
+
33
+ return f"CREATE TABLE {table_name} ({', '.join(columns)})"
34
+
35
+ def visit_drop_table(self, drop: Any, **kw: Any) -> str:
36
+ """Compile DROP TABLE."""
37
+ table = drop.element
38
+ table_name = self.preparer.format_table(table)
39
+ return f"DROP TABLE {table_name}"
40
+
41
+ def visit_create_index(self, create: Any, **kw: Any) -> str:
42
+ raise exc.CompileError("Excel dialect does not support CREATE INDEX")
43
+
44
+ def visit_drop_index(self, drop: Any, **kw: Any) -> str:
45
+ raise exc.CompileError("Excel dialect does not support DROP INDEX")
46
+
47
+ def visit_add_constraint(self, create: Any, **kw: Any) -> str:
48
+ raise exc.CompileError("Excel dialect does not support constraints")
49
+
50
+ def visit_drop_constraint(self, drop: Any, **kw: Any) -> str:
51
+ raise exc.CompileError("Excel dialect does not support constraints")
52
+
53
+ def visit_create_sequence(self, create: Any, **kw: Any) -> str:
54
+ raise exc.CompileError("Excel dialect does not support sequences")
55
+
56
+ def visit_drop_sequence(self, drop: Any, **kw: Any) -> str:
57
+ raise exc.CompileError("Excel dialect does not support sequences")