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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(.venv/bin/python -m pytest -q)"
5
+ ]
6
+ }
7
+ }
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
+ ![Parameter specifications in EDJAS](images/parameters.png "Parameter specifications in EDJAS")
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
+ ![Parameter data extracted from a spreadsheet](images/json.png "The parameter data")
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
+ ![Parameter specifications in EDJAS](images/parameters.png "Parameter specifications in EDJAS")
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
+ ![Parameter data extracted from a spreadsheet](images/json.png "The parameter data")
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
+ ]