excel-dbapi 0.1.4__tar.gz → 2.0.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.
- excel_dbapi-2.0.0/PKG-INFO +22 -0
- excel_dbapi-2.0.0/README.md +92 -0
- excel_dbapi-2.0.0/pyproject.toml +42 -0
- excel_dbapi-2.0.0/setup.cfg +4 -0
- excel_dbapi-2.0.0/src/excel_dbapi/connection.py +107 -0
- excel_dbapi-2.0.0/src/excel_dbapi/cursor.py +84 -0
- excel_dbapi-2.0.0/src/excel_dbapi/engine/__init__.py +5 -0
- excel_dbapi-2.0.0/src/excel_dbapi/engine/base.py +18 -0
- excel_dbapi-2.0.0/src/excel_dbapi/engine/executor.py +40 -0
- excel_dbapi-2.0.0/src/excel_dbapi/engine/openpyxl_engine.py +39 -0
- excel_dbapi-2.0.0/src/excel_dbapi/engine/pandas_engine.py +49 -0
- excel_dbapi-2.0.0/src/excel_dbapi/engine/parser.py +49 -0
- excel_dbapi-2.0.0/src/excel_dbapi/exceptions.py +52 -0
- excel_dbapi-2.0.0/src/excel_dbapi.egg-info/PKG-INFO +22 -0
- excel_dbapi-2.0.0/src/excel_dbapi.egg-info/SOURCES.txt +25 -0
- excel_dbapi-2.0.0/src/excel_dbapi.egg-info/dependency_links.txt +1 -0
- excel_dbapi-2.0.0/src/excel_dbapi.egg-info/requires.txt +15 -0
- excel_dbapi-2.0.0/src/excel_dbapi.egg-info/top_level.txt +1 -0
- excel_dbapi-2.0.0/tests/test_connection.py +71 -0
- excel_dbapi-2.0.0/tests/test_cursor.py +23 -0
- excel_dbapi-2.0.0/tests/test_engine.py +55 -0
- excel_dbapi-2.0.0/tests/test_exceptions.py +17 -0
- excel_dbapi-2.0.0/tests/test_executor.py +27 -0
- excel_dbapi-2.0.0/tests/test_integration.py +1 -0
- excel_dbapi-2.0.0/tests/test_parser.py +53 -0
- excel_dbapi-0.1.4/.gitignore +0 -91
- excel_dbapi-0.1.4/PKG-INFO +0 -21
- excel_dbapi-0.1.4/excel_dbapi/__init__.py +0 -3
- excel_dbapi-0.1.4/excel_dbapi/api.py +0 -9
- excel_dbapi-0.1.4/excel_dbapi/connection.py +0 -75
- excel_dbapi-0.1.4/excel_dbapi/cursor.py +0 -75
- excel_dbapi-0.1.4/excel_dbapi/engines/openpyxl_engine.py +0 -46
- excel_dbapi-0.1.4/excel_dbapi/exceptions.py +0 -34
- excel_dbapi-0.1.4/excel_dbapi/query.py +0 -54
- excel_dbapi-0.1.4/excel_dbapi/remote.py +0 -17
- excel_dbapi-0.1.4/excel_dbapi/table.py +0 -53
- excel_dbapi-0.1.4/pyproject.toml +0 -26
- {excel_dbapi-0.1.4 → excel_dbapi-2.0.0}/LICENSE +0 -0
- {excel_dbapi-0.1.4/excel_dbapi/engines → excel_dbapi-2.0.0/src/excel_dbapi}/__init__.py +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: excel-dbapi
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: PEP 249 compliant DB-API driver for Excel files
|
|
5
|
+
Author-email: Yeongseon Choe <yeongseon.choe@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pandas<3.0.0,>=2.0.0
|
|
9
|
+
Requires-Dist: openpyxl<4.0.0,>=3.1.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest<9.0.0,>=8.3.5; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov<5.0.0,>=4.1; extra == "dev"
|
|
13
|
+
Requires-Dist: black<26.0.0,>=25.1.0; extra == "dev"
|
|
14
|
+
Requires-Dist: isort<7.0.0,>=6.0.1; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff<0.12.0,>=0.11.2; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy<2.0.0,>=1.15.0; extra == "dev"
|
|
17
|
+
Requires-Dist: build; extra == "dev"
|
|
18
|
+
Requires-Dist: bandit<2.0.0,>=1.7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: vulture<3.0,>=2.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
21
|
+
Requires-Dist: types-requests<3.0.0,>=2.28.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
|
|
2
|
+
# excel-dbapi
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+
[](https://codecov.io/gh/your-username/excel-dbapi)
|
|
6
|
+
|
|
7
|
+
A lightweight, Python DB-API 2.0 compliant connector for Excel files.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Python DB-API 2.0 compliant interface
|
|
14
|
+
- Query Excel files using SQL syntax
|
|
15
|
+
- Supports SELECT (INSERT, UPDATE, DELETE planned)
|
|
16
|
+
- Sheet-to-Table mapping
|
|
17
|
+
- Pandas & Openpyxl engine selector
|
|
18
|
+
- Transaction simulation (planned)
|
|
19
|
+
- SQLAlchemy Dialect integration (planned)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install excel-dbapi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See [CHANGELOG](CHANGELOG.md) for release history.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Basic Usage (Local File)
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from excel_dbapi.connection import ExcelConnection
|
|
39
|
+
|
|
40
|
+
# Using default engine (openpyxl)
|
|
41
|
+
with ExcelConnection("path/to/sample.xlsx") as conn:
|
|
42
|
+
cursor = conn.cursor()
|
|
43
|
+
cursor.execute("SELECT * FROM Sheet1")
|
|
44
|
+
print(cursor.fetchall())
|
|
45
|
+
|
|
46
|
+
# Using pandas engine
|
|
47
|
+
with ExcelConnection("path/to/sample.xlsx", engine="pandas") as conn:
|
|
48
|
+
cursor = conn.cursor()
|
|
49
|
+
cursor.execute("SELECT * FROM Sheet1")
|
|
50
|
+
print(cursor.fetchall())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Engine Options
|
|
54
|
+
|
|
55
|
+
| Engine | Description | Dependency |
|
|
56
|
+
|---------|------------------------------|--------------|
|
|
57
|
+
| openpyxl (default) | Fast sheet access (read-only) | openpyxl |
|
|
58
|
+
| pandas | DataFrame based operations | pandas, openpyxl |
|
|
59
|
+
|
|
60
|
+
You can explicitly specify the engine using:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
conn = ExcelConnection("sample.xlsx", engine="openpyxl")
|
|
64
|
+
conn = ExcelConnection("sample.xlsx", engine="pandas")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Planned Features
|
|
70
|
+
|
|
71
|
+
- Write operations (INSERT, UPDATE, DELETE)
|
|
72
|
+
- DDL support (CREATE TABLE, DROP TABLE)
|
|
73
|
+
- Transaction simulation
|
|
74
|
+
- Advanced SQL condition support (WHERE, ORDER BY, LIMIT)
|
|
75
|
+
- Remote file connection support
|
|
76
|
+
- SQLAlchemy Dialect
|
|
77
|
+
|
|
78
|
+
See [Project Roadmap](docs/ROADMAP.md) for details.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
- [Usage Guide](docs/USAGE.md)
|
|
85
|
+
- [Development Guide](docs/DEVELOPMENT.md)
|
|
86
|
+
- [Project Roadmap](docs/ROADMAP.md)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT License
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "excel-dbapi"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "PEP 249 compliant DB-API driver for Excel files"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pandas>=2.0.0,<3.0.0",
|
|
12
|
+
"openpyxl>=3.1.0,<4.0.0"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[[project.authors]]
|
|
16
|
+
name = "Yeongseon Choe"
|
|
17
|
+
email = "yeongseon.choe@gmail.com"
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.3.5,<9.0.0",
|
|
22
|
+
"pytest-cov>=4.1,<5.0.0",
|
|
23
|
+
"black>=25.1.0,<26.0.0",
|
|
24
|
+
"isort>=6.0.1,<7.0.0",
|
|
25
|
+
"ruff>=0.11.2,<0.12.0",
|
|
26
|
+
"mypy>=1.15.0,<2.0.0",
|
|
27
|
+
"build",
|
|
28
|
+
"bandit>=1.7.0,<2.0.0",
|
|
29
|
+
"vulture>=2.0,<3.0",
|
|
30
|
+
"pre-commit",
|
|
31
|
+
"types-requests>=2.28.0,<3.0.0"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.mypy]
|
|
35
|
+
ignore_missing_imports = true
|
|
36
|
+
strict = false
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Optional, Type
|
|
2
|
+
|
|
3
|
+
from .cursor import ExcelCursor
|
|
4
|
+
from .engine import BaseEngine, OpenpyxlEngine, PandasEngine
|
|
5
|
+
from .exceptions import InterfaceError, NotSupportedError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_closed(func):
|
|
9
|
+
"""Decorator to check if connection is closed before executing method."""
|
|
10
|
+
|
|
11
|
+
def wrapper(self, *args, **kwargs):
|
|
12
|
+
if self.closed:
|
|
13
|
+
raise InterfaceError("Connection is already closed")
|
|
14
|
+
return func(self, *args, **kwargs)
|
|
15
|
+
|
|
16
|
+
return wrapper
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExcelConnection:
|
|
20
|
+
"""
|
|
21
|
+
ExcelConnection provides a PEP 249 compliant Connection interface
|
|
22
|
+
for reading and querying Excel files.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, file_path: str, engine: str = "openpyxl"):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the connection with the Excel file and selected engine.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
file_path (str): Path to the Excel file.
|
|
31
|
+
engine (str): Engine type ('pandas' or 'openpyxl').
|
|
32
|
+
"""
|
|
33
|
+
self.file_path: str = file_path
|
|
34
|
+
self.closed: bool = False
|
|
35
|
+
|
|
36
|
+
if engine == "pandas":
|
|
37
|
+
self.engine: BaseEngine = PandasEngine(file_path)
|
|
38
|
+
elif engine == "openpyxl":
|
|
39
|
+
self.engine: BaseEngine = OpenpyxlEngine(file_path)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Unsupported engine: {engine}")
|
|
42
|
+
|
|
43
|
+
@check_closed
|
|
44
|
+
def cursor(self) -> ExcelCursor:
|
|
45
|
+
"""
|
|
46
|
+
Return a new Cursor object using the connection.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
ExcelCursor: A new cursor object.
|
|
50
|
+
"""
|
|
51
|
+
return ExcelCursor(self.engine)
|
|
52
|
+
|
|
53
|
+
@check_closed
|
|
54
|
+
def commit(self) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Commit any pending transaction (Not supported for Excel).
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
NotSupportedError: Always raised because transactions are not supported.
|
|
60
|
+
"""
|
|
61
|
+
raise NotSupportedError("Transactions are not supported")
|
|
62
|
+
|
|
63
|
+
@check_closed
|
|
64
|
+
def rollback(self) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Roll back to the start of any pending transaction (Not supported for Excel).
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
NotSupportedError: Always raised because transactions are not supported.
|
|
70
|
+
"""
|
|
71
|
+
raise NotSupportedError("Transactions are not supported")
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Close the connection.
|
|
76
|
+
"""
|
|
77
|
+
self.closed = True
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def engine_name(self) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Return the name of the engine being used.
|
|
83
|
+
"""
|
|
84
|
+
return self.engine.__class__.__name__
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return f"<ExcelConnection file='{self.file_path}' engine='{self.engine_name}' closed={self.closed}>"
|
|
88
|
+
|
|
89
|
+
def __repr__(self) -> str:
|
|
90
|
+
return self.__str__()
|
|
91
|
+
|
|
92
|
+
def __enter__(self) -> "ExcelConnection":
|
|
93
|
+
"""
|
|
94
|
+
Enter the runtime context related to this object.
|
|
95
|
+
"""
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(
|
|
99
|
+
self,
|
|
100
|
+
exc_type: Optional[Type[BaseException]],
|
|
101
|
+
exc_val: Optional[BaseException],
|
|
102
|
+
exc_tb,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Exit the runtime context and close the connection.
|
|
106
|
+
"""
|
|
107
|
+
self.close()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from .engine.executor import execute_query
|
|
4
|
+
from .engine.parser import parse_sql
|
|
5
|
+
from .exceptions import InterfaceError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_closed(func):
|
|
9
|
+
"""Decorator to check if cursor is closed before executing method."""
|
|
10
|
+
|
|
11
|
+
def wrapper(self, *args, **kwargs):
|
|
12
|
+
if self.closed:
|
|
13
|
+
raise InterfaceError("Cursor is already closed")
|
|
14
|
+
return func(self, *args, **kwargs)
|
|
15
|
+
|
|
16
|
+
return wrapper
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExcelCursor:
|
|
20
|
+
"""
|
|
21
|
+
ExcelCursor provides a PEP 249 compliant Cursor interface
|
|
22
|
+
for executing SQL-like queries on Excel data.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, connection: Any):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the cursor with a connection.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
connection (ExcelConnection): The parent connection object.
|
|
31
|
+
"""
|
|
32
|
+
self.connection = connection
|
|
33
|
+
self.closed: bool = False
|
|
34
|
+
self._results: List[Dict[str, Any]] = []
|
|
35
|
+
self._index: int = 0
|
|
36
|
+
|
|
37
|
+
@check_closed
|
|
38
|
+
def execute(self, query: str, params: Optional[tuple] = None) -> "ExcelCursor":
|
|
39
|
+
"""
|
|
40
|
+
Execute a SQL query.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query (str): The SQL query string.
|
|
44
|
+
params (Optional[tuple]): Parameters to bind to query placeholders.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ExcelCursor: The cursor itself.
|
|
48
|
+
"""
|
|
49
|
+
parsed = parse_sql(query, params)
|
|
50
|
+
self._results = execute_query(parsed, self.connection.data)
|
|
51
|
+
self._index = 0
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
@check_closed
|
|
55
|
+
def fetchone(self) -> Optional[Dict[str, Any]]:
|
|
56
|
+
"""
|
|
57
|
+
Fetch the next row of a query result.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Optional[Dict[str, Any]]: The next row or None if no more rows.
|
|
61
|
+
"""
|
|
62
|
+
if self._index >= len(self._results):
|
|
63
|
+
return None
|
|
64
|
+
result = self._results[self._index]
|
|
65
|
+
self._index += 1
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
@check_closed
|
|
69
|
+
def fetchall(self) -> List[Dict[str, Any]]:
|
|
70
|
+
"""
|
|
71
|
+
Fetch all remaining rows of a query result.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List[Dict[str, Any]]: List of all remaining rows.
|
|
75
|
+
"""
|
|
76
|
+
results = self._results[self._index :]
|
|
77
|
+
self._index = len(self._results)
|
|
78
|
+
return results
|
|
79
|
+
|
|
80
|
+
def close(self) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Close the cursor.
|
|
83
|
+
"""
|
|
84
|
+
self.closed = True
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseEngine(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def load(self) -> Dict[str, Any]:
|
|
8
|
+
"""
|
|
9
|
+
Load data from the Excel file.
|
|
10
|
+
"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def execute(self, query: str) -> List[Dict[str, Any]]:
|
|
15
|
+
"""
|
|
16
|
+
Execute a query against the loaded data.
|
|
17
|
+
"""
|
|
18
|
+
pass
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from pandas import DataFrame
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def execute_query(
|
|
7
|
+
parsed: Dict[str, Any], data: Dict[str, DataFrame]
|
|
8
|
+
) -> List[Dict[str, Any]]:
|
|
9
|
+
"""
|
|
10
|
+
Execute the parsed SQL query against the provided Excel data.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
parsed (Dict[str, Any]): Parsed SQL query components.
|
|
14
|
+
data (Dict[str, DataFrame]): Excel sheet data as DataFrames.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
List[Dict[str, Any]]: Query result as list of dictionaries.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If the specified sheet (table) is not found.
|
|
21
|
+
NotImplementedError: For unsupported SQL actions.
|
|
22
|
+
"""
|
|
23
|
+
table = parsed.get("table")
|
|
24
|
+
if table not in data:
|
|
25
|
+
raise ValueError(f"Sheet '{table}' not found in Excel")
|
|
26
|
+
|
|
27
|
+
if parsed["action"] == "SELECT":
|
|
28
|
+
df = data[table]
|
|
29
|
+
if parsed.get("where"):
|
|
30
|
+
try:
|
|
31
|
+
df = df.query(parsed["where"])
|
|
32
|
+
except Exception as e:
|
|
33
|
+
raise ValueError(f"Invalid WHERE condition: {parsed['where']}") from e
|
|
34
|
+
return df.to_dict(orient="records")
|
|
35
|
+
|
|
36
|
+
elif parsed["action"] == "INSERT":
|
|
37
|
+
raise NotImplementedError("INSERT is not yet implemented")
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
raise NotImplementedError(f"Unsupported action: {parsed['action']}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from openpyxl import load_workbook
|
|
4
|
+
|
|
5
|
+
from .base import BaseEngine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenpyxlEngine(BaseEngine):
|
|
9
|
+
def __init__(self, file_path: str):
|
|
10
|
+
"""
|
|
11
|
+
Initialize OpenpyxlEngine with the given file path.
|
|
12
|
+
"""
|
|
13
|
+
self.file_path = file_path
|
|
14
|
+
self.data = self.load()
|
|
15
|
+
|
|
16
|
+
def load(self) -> Dict[str, Any]:
|
|
17
|
+
"""
|
|
18
|
+
Load all sheets using openpyxl.
|
|
19
|
+
"""
|
|
20
|
+
wb = load_workbook(self.file_path, data_only=True)
|
|
21
|
+
return {sheet: wb[sheet] for sheet in wb.sheetnames}
|
|
22
|
+
|
|
23
|
+
def save(self) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Save is not implemented for OpenpyxlEngine.
|
|
26
|
+
"""
|
|
27
|
+
raise NotImplementedError(
|
|
28
|
+
"Save operation is not implemented for OpenpyxlEngine."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def execute(self, query: str) -> List[Dict[str, Any]]:
|
|
32
|
+
"""
|
|
33
|
+
Example execution: return all records from the first sheet.
|
|
34
|
+
"""
|
|
35
|
+
sheet = list(self.data.keys())[0]
|
|
36
|
+
ws = self.data[sheet]
|
|
37
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
38
|
+
headers = rows[0]
|
|
39
|
+
return [dict(zip(headers, row)) for row in rows[1:]]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from pandas import DataFrame
|
|
5
|
+
|
|
6
|
+
from .base import BaseEngine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PandasEngine(BaseEngine):
|
|
10
|
+
"""
|
|
11
|
+
PandasEngine uses pandas with openpyxl backend to load and query Excel files.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, file_path: str):
|
|
15
|
+
"""
|
|
16
|
+
Initialize PandasEngine with the given file path.
|
|
17
|
+
"""
|
|
18
|
+
self.file_path = file_path
|
|
19
|
+
self.data = self.load()
|
|
20
|
+
|
|
21
|
+
def load(self) -> Dict[str, DataFrame]:
|
|
22
|
+
"""
|
|
23
|
+
Load all sheets as DataFrames using pandas with openpyxl engine.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
data = pd.read_excel(self.file_path, sheet_name=None, engine="openpyxl")
|
|
27
|
+
return data
|
|
28
|
+
except Exception as e:
|
|
29
|
+
raise IOError(f"Failed to load Excel file: {self.file_path}") from e
|
|
30
|
+
|
|
31
|
+
def save(self, file_path: str) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Save the current data back to an Excel file.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
with pd.ExcelWriter(file_path, engine="openpyxl") as writer:
|
|
37
|
+
for sheet_name, df in self.data.items():
|
|
38
|
+
df.to_excel(writer, sheet_name=sheet_name, index=False)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise IOError(f"Failed to save Excel file: {file_path}") from e
|
|
41
|
+
|
|
42
|
+
def execute(self, query: str) -> list[dict[str, Any]]:
|
|
43
|
+
"""
|
|
44
|
+
Execute a query and return the result.
|
|
45
|
+
|
|
46
|
+
Currently, only SELECT * FROM [Sheet$] is supported as an example.
|
|
47
|
+
"""
|
|
48
|
+
sheet = list(self.data.keys())[0]
|
|
49
|
+
return self.data[sheet].to_dict(orient="records")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Dict, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_sql(query: str, params: Optional[Tuple[Any, ...]] = None) -> Dict[str, Any]:
|
|
6
|
+
query = query.strip()
|
|
7
|
+
|
|
8
|
+
# Parameter binding
|
|
9
|
+
if params:
|
|
10
|
+
placeholders = re.findall(r"\?", query)
|
|
11
|
+
if len(placeholders) != len(params):
|
|
12
|
+
raise ValueError(
|
|
13
|
+
f"Expected {len(placeholders)} parameters, got {len(params)}"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
for param in params:
|
|
17
|
+
if isinstance(param, str):
|
|
18
|
+
value = f"'{param}'"
|
|
19
|
+
else:
|
|
20
|
+
value = str(param)
|
|
21
|
+
query = query.replace("?", value, 1)
|
|
22
|
+
|
|
23
|
+
result: Dict[str, Any] = {}
|
|
24
|
+
lower_query = query.lower()
|
|
25
|
+
|
|
26
|
+
if lower_query.startswith("select"):
|
|
27
|
+
result["action"] = "SELECT"
|
|
28
|
+
table_match = re.search(r"from\s+\[?(\w+)\$?\]?", query, re.IGNORECASE)
|
|
29
|
+
result["table"] = table_match.group(1).lower() if table_match else None
|
|
30
|
+
|
|
31
|
+
where_match = re.search(r"where\s+(.+)", query, re.IGNORECASE)
|
|
32
|
+
if where_match:
|
|
33
|
+
condition = where_match.group(1).strip()
|
|
34
|
+
# SQL → pandas 변환
|
|
35
|
+
condition = re.sub(r"(?<![=!<>])=(?!=)", "==", condition)
|
|
36
|
+
result["where"] = condition
|
|
37
|
+
else:
|
|
38
|
+
result["where"] = None
|
|
39
|
+
|
|
40
|
+
elif lower_query.startswith("insert"):
|
|
41
|
+
result["action"] = "INSERT"
|
|
42
|
+
table_match = re.search(r"into\s+\[?(\w+)\$?\]?", query, re.IGNORECASE)
|
|
43
|
+
result["table"] = table_match.group(1).lower() if table_match else None
|
|
44
|
+
|
|
45
|
+
else:
|
|
46
|
+
raise NotImplementedError(f"Unsupported SQL: {query}")
|
|
47
|
+
|
|
48
|
+
result["query"] = query
|
|
49
|
+
return result
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class Error(Exception):
|
|
2
|
+
"""Base class for all database-related exceptions."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DatabaseError(Error):
|
|
8
|
+
"""Exception for errors related to the database."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InterfaceError(Error):
|
|
14
|
+
"""Exception for errors related to the database interface."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataError(DatabaseError):
|
|
20
|
+
"""Exception for errors due to problems with the processed data."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OperationalError(DatabaseError):
|
|
26
|
+
"""Exception for errors related to the database's operation."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IntegrityError(DatabaseError):
|
|
32
|
+
"""Exception for errors related to data integrity."""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InternalError(DatabaseError):
|
|
38
|
+
"""Exception for internal database errors."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProgrammingError(DatabaseError):
|
|
44
|
+
"""Exception for programming errors (SQL syntax, etc.)."""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NotSupportedError(DatabaseError):
|
|
50
|
+
"""Exception for unsupported features."""
|
|
51
|
+
|
|
52
|
+
pass
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: excel-dbapi
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: PEP 249 compliant DB-API driver for Excel files
|
|
5
|
+
Author-email: Yeongseon Choe <yeongseon.choe@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pandas<3.0.0,>=2.0.0
|
|
9
|
+
Requires-Dist: openpyxl<4.0.0,>=3.1.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest<9.0.0,>=8.3.5; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov<5.0.0,>=4.1; extra == "dev"
|
|
13
|
+
Requires-Dist: black<26.0.0,>=25.1.0; extra == "dev"
|
|
14
|
+
Requires-Dist: isort<7.0.0,>=6.0.1; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff<0.12.0,>=0.11.2; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy<2.0.0,>=1.15.0; extra == "dev"
|
|
17
|
+
Requires-Dist: build; extra == "dev"
|
|
18
|
+
Requires-Dist: bandit<2.0.0,>=1.7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: vulture<3.0,>=2.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
21
|
+
Requires-Dist: types-requests<3.0.0,>=2.28.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/excel_dbapi/__init__.py
|
|
5
|
+
src/excel_dbapi/connection.py
|
|
6
|
+
src/excel_dbapi/cursor.py
|
|
7
|
+
src/excel_dbapi/exceptions.py
|
|
8
|
+
src/excel_dbapi.egg-info/PKG-INFO
|
|
9
|
+
src/excel_dbapi.egg-info/SOURCES.txt
|
|
10
|
+
src/excel_dbapi.egg-info/dependency_links.txt
|
|
11
|
+
src/excel_dbapi.egg-info/requires.txt
|
|
12
|
+
src/excel_dbapi.egg-info/top_level.txt
|
|
13
|
+
src/excel_dbapi/engine/__init__.py
|
|
14
|
+
src/excel_dbapi/engine/base.py
|
|
15
|
+
src/excel_dbapi/engine/executor.py
|
|
16
|
+
src/excel_dbapi/engine/openpyxl_engine.py
|
|
17
|
+
src/excel_dbapi/engine/pandas_engine.py
|
|
18
|
+
src/excel_dbapi/engine/parser.py
|
|
19
|
+
tests/test_connection.py
|
|
20
|
+
tests/test_cursor.py
|
|
21
|
+
tests/test_engine.py
|
|
22
|
+
tests/test_exceptions.py
|
|
23
|
+
tests/test_executor.py
|
|
24
|
+
tests/test_integration.py
|
|
25
|
+
tests/test_parser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pandas<3.0.0,>=2.0.0
|
|
2
|
+
openpyxl<4.0.0,>=3.1.0
|
|
3
|
+
|
|
4
|
+
[dev]
|
|
5
|
+
pytest<9.0.0,>=8.3.5
|
|
6
|
+
pytest-cov<5.0.0,>=4.1
|
|
7
|
+
black<26.0.0,>=25.1.0
|
|
8
|
+
isort<7.0.0,>=6.0.1
|
|
9
|
+
ruff<0.12.0,>=0.11.2
|
|
10
|
+
mypy<2.0.0,>=1.15.0
|
|
11
|
+
build
|
|
12
|
+
bandit<2.0.0,>=1.7.0
|
|
13
|
+
vulture<3.0,>=2.0
|
|
14
|
+
pre-commit
|
|
15
|
+
types-requests<3.0.0,>=2.28.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
excel_dbapi
|