edjas 0.5.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.
- edjas-0.5.1/.claude/settings.local.json +7 -0
- edjas-0.5.1/.gitignore +134 -0
- edjas-0.5.1/LICENSE +21 -0
- edjas-0.5.1/PKG-INFO +71 -0
- edjas-0.5.1/README.md +48 -0
- edjas-0.5.1/images/json.png +0 -0
- edjas-0.5.1/images/parameters.png +0 -0
- edjas-0.5.1/pyproject.toml +45 -0
- edjas-0.5.1/scripts/create_fixtures.py +77 -0
- edjas-0.5.1/src/edjas/__init__.py +35 -0
- edjas-0.5.1/src/edjas/find_first.py +49 -0
- edjas-0.5.1/src/edjas/read_params.py +74 -0
- edjas-0.5.1/tests/data/parameters.xlsx +0 -0
- edjas-0.5.1/tests/test_read_params.py +62 -0
- edjas-0.5.1/uv.lock +104 -0
edjas-0.5.1/.gitignore
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
.DS_Store
|
|
3
|
+
# Byte-compiled / optimized / DLL files
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
pip-wheel-metadata/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
# Usually these files are written by a python script from a template
|
|
33
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
# IPython
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
# pyenv
|
|
85
|
+
.python-version
|
|
86
|
+
|
|
87
|
+
# pipenv
|
|
88
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
89
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
90
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
91
|
+
# install all needed dependencies.
|
|
92
|
+
#Pipfile.lock
|
|
93
|
+
|
|
94
|
+
# celery beat schedule file
|
|
95
|
+
celerybeat-schedule
|
|
96
|
+
|
|
97
|
+
# SageMath parsed files
|
|
98
|
+
*.sage.py
|
|
99
|
+
|
|
100
|
+
# Environments
|
|
101
|
+
.env
|
|
102
|
+
.venv
|
|
103
|
+
env/
|
|
104
|
+
venv/
|
|
105
|
+
ENV/
|
|
106
|
+
env.bak/
|
|
107
|
+
venv.bak/
|
|
108
|
+
|
|
109
|
+
# Spyder project settings
|
|
110
|
+
.spyderproject
|
|
111
|
+
.spyproject
|
|
112
|
+
|
|
113
|
+
# Rope project settings
|
|
114
|
+
.ropeproject
|
|
115
|
+
|
|
116
|
+
# mkdocs documentation
|
|
117
|
+
/site
|
|
118
|
+
|
|
119
|
+
# mypy
|
|
120
|
+
.mypy_cache/
|
|
121
|
+
.dmypy.json
|
|
122
|
+
dmypy.json
|
|
123
|
+
|
|
124
|
+
# Pyre type checker
|
|
125
|
+
.pyre/
|
|
126
|
+
|
|
127
|
+
# WingPro project files
|
|
128
|
+
*.wp?
|
|
129
|
+
|
|
130
|
+
release-*.tgz
|
|
131
|
+
dist/
|
|
132
|
+
|
|
133
|
+
# Wing IDE remote-debugger stub (dev-only, not part of the package)
|
|
134
|
+
wingdbstub.py
|
edjas-0.5.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Steve Holden
|
|
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.
|
edjas-0.5.1/PKG-INFO
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: edjas
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Extract data in JSON from any spreadsheet
|
|
5
|
+
Author-email: Steve Holden <steve@holdenweb.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: data-extraction,excel,json,openpyxl,reporting,spreadsheet,xlsx
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: openpyxl<4.0.0,>=3.1.5
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# EDJAS: Extract Data in JSON from Any Spreadsheet
|
|
25
|
+
|
|
26
|
+
- Sources at https://github.com/holdenweb/edjas
|
|
27
|
+
|
|
28
|
+
This project is an attempt to help organisations that insist on managing
|
|
29
|
+
their businesses, or major aspects thereof, using spreadsheets.
|
|
30
|
+
Many articles have been written on the limitations of spreadsheet technology.
|
|
31
|
+
If you have any doubts then look at the "The Problem with Spreadsheets"
|
|
32
|
+
section of this [LinkedIn
|
|
33
|
+
article](https://www.linkedin.com/pulse/spreadsheets-inadequate-effective-management--gjsse/).
|
|
34
|
+
Some large organisations are now
|
|
35
|
+
[providing advice](https://www.gov.uk/guidance/creating-and-sharing-spreadsheets)
|
|
36
|
+
— although in many cases better advice might be:
|
|
37
|
+
_stop using spreadsheets for that_!
|
|
38
|
+
|
|
39
|
+
Rather than try to change the way people do business (imagine "If I Ruled the
|
|
40
|
+
World" playing softly in the background), EDJAS is intended to help people extract
|
|
41
|
+
that locked-up data more effectively, in simple and easy-to-understand ways
|
|
42
|
+
that don't affect existing workflows.
|
|
43
|
+
|
|
44
|
+
It lets you add data specifications to any existing spreadsheet by creating
|
|
45
|
+
named ranges in the spreadsheet. By default EDJAS will look for a range
|
|
46
|
+
name `Parameters` as its starting point, although this can be overridden on the command line.
|
|
47
|
+
This range should be precisely two columns wide, and EDJAS
|
|
48
|
+
treats the left-hand column as names and the right-hand column as values.
|
|
49
|
+
Normally, the values are used literally after extraction from the spreadsheet.
|
|
50
|
+
Two formats for the value are given special treatment.
|
|
51
|
+
|
|
52
|
+
- `[range-name]`: the named range is exported as a JSON list or, if it's two-dimensional a list of row lists.
|
|
53
|
+
- `{range_name}`: The named range, which must be two columns wide, becomes a JSON object where the left-hand column specifies
|
|
54
|
+
the names and the right-hand column specifies the values.
|
|
55
|
+
|
|
56
|
+
The parameter details are used to extract data from the spreadsheet, which is then sent to standard output as JSON.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
In the example shown, the `version` key has a dict value, and in that dict the `number` key has a value of "1.0.2".
|
|
61
|
+
The version number can therefore be referenced in the JSON output as `version.number`. The output from this example is shown below.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
A demonstration of the system can be found at [https://github.com/holdenweb/edjas-demo](https://github.com/holdenweb/edjas-demo).
|
|
67
|
+
|
|
68
|
+
This is particularly useful for audiences that have an interest in only a
|
|
69
|
+
limited number of features from a possibly quite large spreadsheet.
|
|
70
|
+
More generally, JSON is such a widely used format that spreadsheet data can
|
|
71
|
+
be re-used in a wide range of systems as appropriate.
|
edjas-0.5.1/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# EDJAS: Extract Data in JSON from Any Spreadsheet
|
|
2
|
+
|
|
3
|
+
- Sources at https://github.com/holdenweb/edjas
|
|
4
|
+
|
|
5
|
+
This project is an attempt to help organisations that insist on managing
|
|
6
|
+
their businesses, or major aspects thereof, using spreadsheets.
|
|
7
|
+
Many articles have been written on the limitations of spreadsheet technology.
|
|
8
|
+
If you have any doubts then look at the "The Problem with Spreadsheets"
|
|
9
|
+
section of this [LinkedIn
|
|
10
|
+
article](https://www.linkedin.com/pulse/spreadsheets-inadequate-effective-management--gjsse/).
|
|
11
|
+
Some large organisations are now
|
|
12
|
+
[providing advice](https://www.gov.uk/guidance/creating-and-sharing-spreadsheets)
|
|
13
|
+
— although in many cases better advice might be:
|
|
14
|
+
_stop using spreadsheets for that_!
|
|
15
|
+
|
|
16
|
+
Rather than try to change the way people do business (imagine "If I Ruled the
|
|
17
|
+
World" playing softly in the background), EDJAS is intended to help people extract
|
|
18
|
+
that locked-up data more effectively, in simple and easy-to-understand ways
|
|
19
|
+
that don't affect existing workflows.
|
|
20
|
+
|
|
21
|
+
It lets you add data specifications to any existing spreadsheet by creating
|
|
22
|
+
named ranges in the spreadsheet. By default EDJAS will look for a range
|
|
23
|
+
name `Parameters` as its starting point, although this can be overridden on the command line.
|
|
24
|
+
This range should be precisely two columns wide, and EDJAS
|
|
25
|
+
treats the left-hand column as names and the right-hand column as values.
|
|
26
|
+
Normally, the values are used literally after extraction from the spreadsheet.
|
|
27
|
+
Two formats for the value are given special treatment.
|
|
28
|
+
|
|
29
|
+
- `[range-name]`: the named range is exported as a JSON list or, if it's two-dimensional a list of row lists.
|
|
30
|
+
- `{range_name}`: The named range, which must be two columns wide, becomes a JSON object where the left-hand column specifies
|
|
31
|
+
the names and the right-hand column specifies the values.
|
|
32
|
+
|
|
33
|
+
The parameter details are used to extract data from the spreadsheet, which is then sent to standard output as JSON.
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
In the example shown, the `version` key has a dict value, and in that dict the `number` key has a value of "1.0.2".
|
|
38
|
+
The version number can therefore be referenced in the JSON output as `version.number`. The output from this example is shown below.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
A demonstration of the system can be found at [https://github.com/holdenweb/edjas-demo](https://github.com/holdenweb/edjas-demo).
|
|
44
|
+
|
|
45
|
+
This is particularly useful for audiences that have an interest in only a
|
|
46
|
+
limited number of features from a possibly quite large spreadsheet.
|
|
47
|
+
More generally, JSON is such a widely used format that spreadsheet data can
|
|
48
|
+
be re-used in a wide range of systems as appropriate.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "edjas"
|
|
3
|
+
version = "0.5.1"
|
|
4
|
+
description = "Extract data in JSON from any spreadsheet"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Steve Holden", email = "steve@holdenweb.com" },
|
|
7
|
+
]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
keywords = ["spreadsheet", "excel", "xlsx", "openpyxl", "reporting", "json", "data-extraction"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Intended Audience :: Information Technology",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Office/Business :: Financial :: Spreadsheet",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"openpyxl>=3.1.5,<4.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/edjas"]
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = ["pytest>=8"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
addopts = "-q"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
edjas = "edjas:main"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Regenerate the test fixture used by the EDJAS test suite.
|
|
2
|
+
|
|
3
|
+
Running this script rewrites ``tests/data/parameters.xlsx`` with the exact
|
|
4
|
+
layout that ``tests/test_read_params.py`` asserts against:
|
|
5
|
+
|
|
6
|
+
* a two-column ``Parameters`` range whose values exercise scalars, ``{dict}``
|
|
7
|
+
references, ``[range]`` references, and direct cell-range references;
|
|
8
|
+
* a ``hours`` named range (a 7-row day/opening-hours table);
|
|
9
|
+
* a ``prices`` named range living on a second ``SubParameters`` sheet.
|
|
10
|
+
|
|
11
|
+
Run from the project root: ``python scripts/create_fixtures.py``
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from openpyxl import Workbook
|
|
17
|
+
from openpyxl.workbook.defined_name import DefinedName
|
|
18
|
+
|
|
19
|
+
FIXTURE = Path(__file__).resolve().parent.parent / "tests" / "data" / "parameters.xlsx"
|
|
20
|
+
|
|
21
|
+
HOURS = [
|
|
22
|
+
("Monday", "7:00 am - 8:00 pm"),
|
|
23
|
+
("Tuesday", "7:00 am - 8:00 pm"),
|
|
24
|
+
("Wednesday", "7:00 am - 8:00 pm"),
|
|
25
|
+
("Thursday", "7:00 am - 8:00 pm"),
|
|
26
|
+
("Friday", "7:00 am - 8:00 pm"),
|
|
27
|
+
("Saturday", "9:00 am - 5:00 pm"),
|
|
28
|
+
("Sunday", "Closed"),
|
|
29
|
+
]
|
|
30
|
+
H_VECTOR = ["the", "quick", "brown", "fox"]
|
|
31
|
+
V_VECTOR = ["jumps", "over", "the", "lazy", "dog"]
|
|
32
|
+
PRICES = [("Tea", 3.25), ("Coffee", 4.0), ("Bacon Sandwich", 8.25)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build():
|
|
36
|
+
wb = Workbook()
|
|
37
|
+
|
|
38
|
+
params = wb.active
|
|
39
|
+
params.title = "Parameters"
|
|
40
|
+
|
|
41
|
+
# Column A/B: the parameter specifications EDJAS reads.
|
|
42
|
+
params["A3"], params["B3"] = "hours", "{hours}"
|
|
43
|
+
params["A4"], params["B4"] = "prices", "{prices}"
|
|
44
|
+
params["A5"], params["B5"] = "title", "Hubris Demo"
|
|
45
|
+
params["A6"], params["B6"] = "hours_list", "[D3:E9]"
|
|
46
|
+
params["A7"], params["B7"] = "h_vector", "[H5:K5]"
|
|
47
|
+
params["A8"], params["B8"] = "v_vector", "[H3:H7]"
|
|
48
|
+
|
|
49
|
+
# Columns D/E: the hours table, referenced by {hours} and [D3:E9].
|
|
50
|
+
for offset, (day, opening) in enumerate(HOURS):
|
|
51
|
+
params.cell(row=3 + offset, column=4, value=day)
|
|
52
|
+
params.cell(row=3 + offset, column=5, value=opening)
|
|
53
|
+
|
|
54
|
+
# Column H rows 3-7: the vertical vector, referenced by [H3:H7].
|
|
55
|
+
for offset, word in enumerate(V_VECTOR):
|
|
56
|
+
params.cell(row=3 + offset, column=8, value=word)
|
|
57
|
+
|
|
58
|
+
# Row 5 columns H-K: the horizontal vector, referenced by [H5:K5].
|
|
59
|
+
for offset, word in enumerate(H_VECTOR):
|
|
60
|
+
params.cell(row=5, column=8 + offset, value=word)
|
|
61
|
+
|
|
62
|
+
sub = wb.create_sheet("SubParameters")
|
|
63
|
+
for offset, (item, price) in enumerate(PRICES):
|
|
64
|
+
sub.cell(row=1 + offset, column=1, value=item)
|
|
65
|
+
sub.cell(row=1 + offset, column=2, value=price)
|
|
66
|
+
|
|
67
|
+
wb.defined_names.add(DefinedName("Parameters", attr_text="Parameters!$A$1:$B$15"))
|
|
68
|
+
wb.defined_names.add(DefinedName("hours", attr_text="Parameters!$D$3:$E$9"))
|
|
69
|
+
wb.defined_names.add(DefinedName("prices", attr_text="SubParameters!$A$1:$B$3"))
|
|
70
|
+
|
|
71
|
+
FIXTURE.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
wb.save(FIXTURE)
|
|
73
|
+
print(f"Wrote fixture: {FIXTURE}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
build()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
from .read_params import read_file
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("edjas")
|
|
11
|
+
except PackageNotFoundError: # not installed (e.g. running from a source checkout)
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
|
13
|
+
|
|
14
|
+
__all__ = ["read_file", "__version__"]
|
|
15
|
+
|
|
16
|
+
def main(argv=None):
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="edjas",
|
|
19
|
+
description="Extract data in JSON from any spreadsheet.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("file", help="path to the spreadsheet to read")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-r",
|
|
24
|
+
"--range",
|
|
25
|
+
default="Parameters",
|
|
26
|
+
help="named range to use as the starting point (default: Parameters)",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--version",
|
|
30
|
+
action="version",
|
|
31
|
+
version=f"%(prog)s {__version__}",
|
|
32
|
+
)
|
|
33
|
+
args = parser.parse_args(argv)
|
|
34
|
+
json.dump(read_file(args.file, args.range), sys.stdout)
|
|
35
|
+
sys.stdout.write("\n")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from openpyxl import Workbook
|
|
2
|
+
from openpyxl.cell import Cell
|
|
3
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def column(sheet: Worksheet) -> Cell:
|
|
7
|
+
"""
|
|
8
|
+
Return the first cell of the first non-blank column in the sheet.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
sheet (openpyxl.worksheet.worksheet.Worksheet): The worksheet to search.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
openpyxl.cell.Cell: The first non-blank cell scanning column by column,
|
|
15
|
+
or None if every cell is blank.
|
|
16
|
+
"""
|
|
17
|
+
for col in sheet.iter_cols():
|
|
18
|
+
for cell in col:
|
|
19
|
+
if cell is not None and cell.value: # Check if the cell is not empty
|
|
20
|
+
return cell
|
|
21
|
+
|
|
22
|
+
def row(sheet: Worksheet) -> Cell:
|
|
23
|
+
"""
|
|
24
|
+
Return the first cell of the first non-blank row in the sheet.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
sheet (openpyxl.worksheet.worksheet.Worksheet): The worksheet to search.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
openpyxl.cell.Cell: The first non-blank cell scanning row by row,
|
|
31
|
+
or None if every cell is blank.
|
|
32
|
+
"""
|
|
33
|
+
for row in sheet.iter_rows():
|
|
34
|
+
for cell in row:
|
|
35
|
+
if cell is not None and cell.value: # Check if the cell is not empty
|
|
36
|
+
return cell
|
|
37
|
+
|
|
38
|
+
def top_left(sheet: Worksheet) -> Cell:
|
|
39
|
+
"""
|
|
40
|
+
Return the top-left cell of the sheet's used data range.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
sheet (openpyxl.worksheet.worksheet.Worksheet): The worksheet to search.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
openpyxl.cell.Cell: The cell at the top-left corner of the used range.
|
|
47
|
+
"""
|
|
48
|
+
data_range = sheet.calculate_dimension()
|
|
49
|
+
return sheet[data_range][0][0]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import openpyxl
|
|
4
|
+
|
|
5
|
+
def extract_values(sheet, range_spec, flatten=True):
|
|
6
|
+
result = []
|
|
7
|
+
for row in sheet[range_spec]:
|
|
8
|
+
result.append([c.value for c in row])
|
|
9
|
+
if not flatten:
|
|
10
|
+
return result
|
|
11
|
+
if len(result) == 1:
|
|
12
|
+
return result[0]
|
|
13
|
+
elif len(result[0]) == 1:
|
|
14
|
+
return [r[0] for r in result]
|
|
15
|
+
else:
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
def range_values(wb, range_spec, flatten=True):
|
|
19
|
+
if range_spec in wb.defined_names:
|
|
20
|
+
range_spec = wb.defined_names[range_spec].attr_text
|
|
21
|
+
if "!" in range_spec:
|
|
22
|
+
sheet_name, cell_refs = range_spec.split("!")
|
|
23
|
+
sheet = wb[sheet_name]
|
|
24
|
+
else:
|
|
25
|
+
sheet = wb.active
|
|
26
|
+
cell_refs = range_spec
|
|
27
|
+
return extract_values(sheet, cell_refs, flatten=flatten)
|
|
28
|
+
|
|
29
|
+
def range_to_dict(workbook, range_spec):
|
|
30
|
+
# Get the rows in the given range
|
|
31
|
+
rows = range_values(workbook, range_spec, flatten=False)
|
|
32
|
+
if len(rows[0]) != 2:
|
|
33
|
+
raise ValueError(f"Range spec {range_spec} should have two columns")
|
|
34
|
+
# Initialize the result dictionary
|
|
35
|
+
result = {}
|
|
36
|
+
i = 0
|
|
37
|
+
for key, value in rows:
|
|
38
|
+
# Skip empty rows, but complain about floating values
|
|
39
|
+
if key is None:
|
|
40
|
+
if value is None:
|
|
41
|
+
continue
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError("Empty key not expected on value {value!r} - programming error?")
|
|
44
|
+
# Check if the value is a range name enclosed in braces: dictionary
|
|
45
|
+
if type(value) is str:
|
|
46
|
+
if value.startswith("{") and value.endswith("}"):
|
|
47
|
+
# Extract the named range name (e.g., "SubParameters" from "{SubParameters}")
|
|
48
|
+
ref_range_spec = value[1:-1]
|
|
49
|
+
# Recursively process the referenced named range
|
|
50
|
+
result[key] = range_to_dict(workbook, ref_range_spec)
|
|
51
|
+
# Otherwise "[range]" references a list or matrix.
|
|
52
|
+
elif value.startswith("[") and value.endswith("]"):
|
|
53
|
+
ref_range_spec = value[1:-1]
|
|
54
|
+
result[key] = range_values(workbook, ref_range_spec)
|
|
55
|
+
else:
|
|
56
|
+
result[key] = value
|
|
57
|
+
else:
|
|
58
|
+
# Single value
|
|
59
|
+
result[key] = value
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
def read_file(file_name, range_name="Parameters"):
|
|
63
|
+
# Load the Excel workbook
|
|
64
|
+
workbook = openpyxl.load_workbook(file_name, data_only=False)
|
|
65
|
+
return range_to_dict(workbook, range_name)
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
if len(sys.argv) < 2:
|
|
69
|
+
sys.exit("Requires spreadsheet arguments")
|
|
70
|
+
if len(sys.argv) > 3:
|
|
71
|
+
sys.exit("Sorry, only handling one or two arguments for now")
|
|
72
|
+
from pprint import pprint
|
|
73
|
+
data = read_file(*sys.argv[1:])
|
|
74
|
+
pprint(data)
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from edjas import read_file
|
|
6
|
+
|
|
7
|
+
FIXTURE = Path(__file__).parent / "data" / "parameters.xlsx"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(scope="module")
|
|
11
|
+
def params():
|
|
12
|
+
return read_file(FIXTURE)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_scalar(params):
|
|
16
|
+
assert params["title"] == "Hubris Demo"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_named_range_dict_hours(params):
|
|
20
|
+
assert params["hours"]["Monday"] == "7:00 am - 8:00 pm"
|
|
21
|
+
assert params["hours"]["Sunday"] == "Closed"
|
|
22
|
+
assert len(params["hours"]) == 7
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_named_range_dict_prices(params):
|
|
26
|
+
assert params["prices"] == {"Tea": 3.25, "Coffee": 4.0, "Bacon Sandwich": 8.25}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_matrix_rows(params):
|
|
30
|
+
assert params["hours_list"][0] == ["Monday", "7:00 am - 8:00 pm"]
|
|
31
|
+
assert params["hours_list"][5] == ["Saturday", "9:00 am - 5:00 pm"]
|
|
32
|
+
assert len(params["hours_list"]) == 7
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_horizontal_vector(params):
|
|
36
|
+
assert params["h_vector"] == ["the", "quick", "brown", "fox"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_vertical_vector(params):
|
|
40
|
+
assert params["v_vector"] == ["jumps", "over", "the", "lazy", "dog"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_expected_keys_present(params):
|
|
44
|
+
assert set(params) >= {"title", "hours", "prices", "hours_list", "h_vector", "v_vector"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_single_entry_dict(tmp_path):
|
|
48
|
+
"""A one-row {dict} range must read as a dict, not be flattened to a vector."""
|
|
49
|
+
import openpyxl
|
|
50
|
+
from openpyxl.workbook.defined_name import DefinedName
|
|
51
|
+
|
|
52
|
+
wb = openpyxl.Workbook()
|
|
53
|
+
ws = wb.active
|
|
54
|
+
ws.title = "Sheet1"
|
|
55
|
+
ws["A1"], ws["B1"] = "version", "{version}"
|
|
56
|
+
ws["A2"], ws["B2"] = "number", "0.1.2"
|
|
57
|
+
wb.defined_names.add(DefinedName("Parameters", attr_text="Sheet1!$A$1:$B$1"))
|
|
58
|
+
wb.defined_names.add(DefinedName("version", attr_text="Sheet1!$A$2:$B$2"))
|
|
59
|
+
path = tmp_path / "single.xlsx"
|
|
60
|
+
wb.save(path)
|
|
61
|
+
|
|
62
|
+
assert read_file(path) == {"version": {"number": "0.1.2"}}
|
edjas-0.5.1/uv.lock
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "colorama"
|
|
7
|
+
version = "0.4.6"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "edjas"
|
|
16
|
+
version = "0.5.1"
|
|
17
|
+
source = { editable = "." }
|
|
18
|
+
dependencies = [
|
|
19
|
+
{ name = "openpyxl" },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[package.dev-dependencies]
|
|
23
|
+
dev = [
|
|
24
|
+
{ name = "pytest" },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[package.metadata]
|
|
28
|
+
requires-dist = [{ name = "openpyxl", specifier = ">=3.1.5,<4.0.0" }]
|
|
29
|
+
|
|
30
|
+
[package.metadata.requires-dev]
|
|
31
|
+
dev = [{ name = "pytest", specifier = ">=8" }]
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "et-xmlfile"
|
|
35
|
+
version = "2.0.0"
|
|
36
|
+
source = { registry = "https://pypi.org/simple" }
|
|
37
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
|
38
|
+
wheels = [
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "iniconfig"
|
|
44
|
+
version = "2.3.0"
|
|
45
|
+
source = { registry = "https://pypi.org/simple" }
|
|
46
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
47
|
+
wheels = [
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[[package]]
|
|
52
|
+
name = "openpyxl"
|
|
53
|
+
version = "3.1.5"
|
|
54
|
+
source = { registry = "https://pypi.org/simple" }
|
|
55
|
+
dependencies = [
|
|
56
|
+
{ name = "et-xmlfile" },
|
|
57
|
+
]
|
|
58
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
|
59
|
+
wheels = [
|
|
60
|
+
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[[package]]
|
|
64
|
+
name = "packaging"
|
|
65
|
+
version = "26.2"
|
|
66
|
+
source = { registry = "https://pypi.org/simple" }
|
|
67
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
68
|
+
wheels = [
|
|
69
|
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[[package]]
|
|
73
|
+
name = "pluggy"
|
|
74
|
+
version = "1.6.0"
|
|
75
|
+
source = { registry = "https://pypi.org/simple" }
|
|
76
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
77
|
+
wheels = [
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[[package]]
|
|
82
|
+
name = "pygments"
|
|
83
|
+
version = "2.20.0"
|
|
84
|
+
source = { registry = "https://pypi.org/simple" }
|
|
85
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
86
|
+
wheels = [
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[[package]]
|
|
91
|
+
name = "pytest"
|
|
92
|
+
version = "9.1.1"
|
|
93
|
+
source = { registry = "https://pypi.org/simple" }
|
|
94
|
+
dependencies = [
|
|
95
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
96
|
+
{ name = "iniconfig" },
|
|
97
|
+
{ name = "packaging" },
|
|
98
|
+
{ name = "pluggy" },
|
|
99
|
+
{ name = "pygments" },
|
|
100
|
+
]
|
|
101
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
102
|
+
wheels = [
|
|
103
|
+
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
|
104
|
+
]
|