Skip to content

Commit 639adca

Browse files
committed
paths: Let it handle paths as JSON keys
#3
1 parent 9fb38fd commit 639adca

13 files changed

+181
-33
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- Can handle deep JSON paths
12+
913
## [0.1.0] - 2020-04-06
1014

1115
### Changed

docs/examples.rst

+15-13
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ Examples
77

88
A guide of:
99

10-
+-------------------------------------+------------------------------------------+
11-
| Pet | SPREADSHEETFORM:SINGLE:pet |
12-
+-------------------------------------+------------------------------------------+
13-
| Toys: | |
14-
+-------------------------------------+------------------------------------------+
15-
| Title | Does it squeak? |
16-
+-------------------------------------+------------------------------------------+
17-
| SPREADSHEETFORM:DOWN:toys:title | SPREADSHEETFORM:DOWN:toys:squeak |
18-
+-------------------------------------+------------------------------------------+
10+
+----------------------------------------+------------------------------------------+
11+
| Pet | SPREADSHEETFORM:SINGLE:pet |
12+
+----------------------------------------+------------------------------------------+
13+
| Toys: | |
14+
+----------------------------------------+------------------------------------------+
15+
| Title | Does it squeak? |
16+
+----------------------------------------+------------------------------------------+
17+
| SPREADSHEETFORM:DOWN:likes/toys:title | SPREADSHEETFORM:DOWN:likes/toys:squeak |
18+
+----------------------------------------+------------------------------------------+
1919

2020
And a spreadsheet of:
2121

@@ -37,8 +37,10 @@ Will map to the data:
3737
3838
{
3939
"pet": "Dog",
40-
"toys": [
41-
{"title": "Plastic bone", "squeak": "Oh Yes"},
42-
{"title": "Tennis Ball", "squeak": "No"},
43-
]
40+
"likes": {
41+
"toys": [
42+
{"title": "Plastic bone", "squeak": "Oh Yes"},
43+
{"title": "Tennis Ball", "squeak": "No"}
44+
]
45+
}
4446
}

docs/guideform/down.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The order of the rows and the order of the items in the JSON list will be the sa
2828

2929
The `itemkey` is the JSON key that the data will appear in in each dictionary.
3030

31-
Note the `listkey` and `itemkey` must be a single key and can not contain paths yet; eg "pet" is acceptable, "pet/title" is not.
31+
See :doc:`JSON Key for information on how to structure those<jsonkey>`.
3232

3333
Example
3434
-------

docs/guideform/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ You then put special values in certain cells to indicate that these cells should
1010

1111
single.rst
1212
down.rst
13+
jsonkey.rst

docs/guideform/jsonkey.rst

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
JSON Keys
2+
=========
3+
4+
In the guideform, you can set JSON Keys at various points.
5+
6+
These set the point in the data that values will be read or written to.
7+
8+
These can be single keys:
9+
10+
Eg:
11+
12+
* `pet`
13+
14+
These can also be several keys split by a slash. In this case, it will traverse down several dictionaries.
15+
16+
Eg:
17+
18+
* `pet/kind`

docs/guideform/single.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ To do this, in your cell put
1313
SPREADSHEETFORM:SINGLE:jsonkey
1414
1515
16-
Note the `jsonkey` must be a single key and can not contain paths yet; eg "pet" is acceptable, "pet/title" is not.
16+
See :doc:`JSON Key for information on how to structure those<jsonkey>`.
1717

1818
Example
1919
-------

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from setuptools import setup
22

3-
install_requires = ["openpyxl>=3.0"]
3+
install_requires = ["openpyxl>=3.0", "jsonpointer"]
44

55
setup(
66
name="spreadsheetforms",

spreadsheetforms/api.py

+18-17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import openpyxl
44

5+
from .util import json_append_deep_value, json_get_deep_value, json_set_deep_value
6+
57

68
def _is_content_a_guide_field(content):
79
return content and content.startswith("SPREADSHEETFORM:")
@@ -73,31 +75,29 @@ def get_data_from_form(guide_filename, in_filename):
7375

7476
# Step 2: Process single configs (easy ones)
7577
for single_config in single_configs.values():
76-
# TODO this only does top level paths - we should add so it can do paths with several levels
77-
data[single_config["path"]] = in_workbook[worksheet.title][
78-
single_config["coordinate"]
79-
].value
78+
json_set_deep_value(
79+
data,
80+
single_config["path"],
81+
in_workbook[worksheet.title][single_config["coordinate"]].value,
82+
)
8083

8184
# Step 3: Process Down Configs
8285
for down_config in down_configs.values():
8386
start_row = down_config[0]["row"]
8487
max_row = in_workbook[worksheet.title].max_row + 1
85-
# TODO this only does top level paths - we should add so it can do paths with several levels
86-
data[down_config[0]["list_path"]] = []
88+
json_set_deep_value(data, down_config[0]["list_path"], [])
8789
for row in range(start_row, max_row + 1):
8890
item = {}
8991
found_anything = False
9092
for this_down_config in down_config:
91-
# TODO this only does top level paths - we should add so it can do paths with several levels
9293
cell = in_workbook[worksheet.title][
9394
this_down_config["column_letter"] + str(row)
9495
]
95-
item[this_down_config["item_path"]] = cell.value
96-
if item[this_down_config["item_path"]]:
96+
json_set_deep_value(item, this_down_config["item_path"], cell.value)
97+
if json_get_deep_value(item, this_down_config["item_path"]):
9798
found_anything = True
9899
if found_anything:
99-
# TODO this only does top level paths - we should add so it can do paths with several levels
100-
data[down_config[0]["list_path"]].append(item)
100+
json_append_deep_value(data, down_config[0]["list_path"], item)
101101

102102
return data
103103

@@ -113,27 +113,28 @@ def put_data_in_form(guide_filename, data, out_filename):
113113

114114
# Step 2: Process single configs (easy ones)
115115
for single_config in single_configs.values():
116-
# TODO this only does top level paths - we should add so it can do paths with several levels
117-
worksheet[single_config["coordinate"]] = data.get(single_config["path"])
116+
worksheet[single_config["coordinate"]] = json_get_deep_value(
117+
data, single_config["path"]
118+
)
118119

119120
# Step 3: Process Down Configs
120121
for down_config in down_configs.values():
121-
datas_to_insert = data.get(down_config[0]["list_path"], [])
122+
datas_to_insert = json_get_deep_value(data, down_config[0]["list_path"])
122123
if isinstance(datas_to_insert, list):
123124
if len(datas_to_insert) == 0:
124125
# we still want to remove the special values from the output spreadsheet
125126
for this_down_config in down_config:
126-
# TODO this only does top level paths - we should add so it can do paths with several levels
127127
worksheet[this_down_config["coordinate"]] = ""
128128
else:
129129
extra_row = 0
130130
for data_to_insert in datas_to_insert:
131131
for this_down_config in down_config:
132-
# TODO this only does top level paths - we should add so it can do paths with several levels
133132
worksheet[
134133
this_down_config["column_letter"]
135134
+ str(this_down_config["row"] + extra_row)
136-
] = data_to_insert.get(this_down_config["item_path"])
135+
] = json_get_deep_value(
136+
data_to_insert, this_down_config["item_path"]
137+
)
137138
extra_row += 1
138139

139140
workbook.save(out_filename)

spreadsheetforms/util.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from jsonpointer import JsonPointerException, resolve_pointer
2+
3+
4+
def json_set_deep_value(data, key, value):
5+
key_bits = key.split("/")
6+
7+
# Have to navigate to right bit of structure, creating dicts as we go
8+
if len(key_bits) > 1:
9+
for idx in range(0, len(key_bits) - 1):
10+
11+
key_bit = key_bits[idx]
12+
if key_bit not in data:
13+
data[key_bit] = {}
14+
15+
data = data[key_bit]
16+
17+
# Finally, set value
18+
data[key_bits[-1]] = value
19+
20+
21+
def json_append_deep_value(data, key, value):
22+
key_bits = key.split("/")
23+
24+
# Have to navigate to right bit of structure, creating dicts as we go
25+
if len(key_bits) > 1:
26+
for idx in range(0, len(key_bits) - 1):
27+
28+
key_bit = key_bits[idx]
29+
if key_bit not in data:
30+
data[key_bit] = {}
31+
32+
data = data[key_bit]
33+
34+
# Finally, append value
35+
data[key_bits[-1]].append(value)
36+
37+
38+
def json_get_deep_value(data, key):
39+
try:
40+
return resolve_pointer(data, "/" + key)
41+
except JsonPointerException:
42+
return None

tests/data/pet1-deep.xlsx

6.29 KB
Binary file not shown.

tests/test_get_data_from_form.py

+25
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,28 @@ def test_1():
2323
{"title": "Marble", "squeak": "No"},
2424
],
2525
} == data
26+
27+
28+
def test_deep():
29+
30+
data = get_data_from_form(
31+
os.path.join(TEST_DATA_DIR, "pet1-deep.xlsx"),
32+
os.path.join(TEST_DATA_DIR, "cat1.xlsx"),
33+
)
34+
35+
assert {
36+
"emits": {"noise": "Miaow Miaow Purr Purr Hiss"},
37+
"pet": {"kind": "Cat"},
38+
"likes": {
39+
"toys": [
40+
{
41+
"human-concerns": {"title": "Bit of string"},
42+
"pet-concerns": {"squeak": "No"},
43+
},
44+
{
45+
"human-concerns": {"title": "Marble"},
46+
"pet-concerns": {"squeak": "No"},
47+
},
48+
]
49+
},
50+
} == data

tests/test_put_data_in_form.py

+33
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,36 @@ def test_1():
3333
assert "Oh Yes" == workbook["Toys"]["B7"].value
3434
assert "Tennis Ball" == workbook["Toys"]["A8"].value
3535
assert "No" == workbook["Toys"]["B8"].value
36+
37+
38+
def test_deep():
39+
40+
outfile = os.path.join(TEST_DATA_OUT_DIR, "put_data_in_form_1_deep.xlsx")
41+
42+
data = {
43+
"emits": {"noise": "Woof Woof"},
44+
"pet": {"kind": "Dog"},
45+
"likes": {
46+
"toys": [
47+
{
48+
"human-concerns": {"title": "Plastic bone"},
49+
"pet-concerns": {"squeak": "Oh Yes"},
50+
},
51+
{
52+
"human-concerns": {"title": "Tennis Ball"},
53+
"pet-concerns": {"squeak": "No"},
54+
},
55+
]
56+
},
57+
}
58+
59+
put_data_in_form(os.path.join(TEST_DATA_DIR, "pet1-deep.xlsx"), data, outfile)
60+
61+
workbook = openpyxl.load_workbook(outfile, read_only=True)
62+
63+
assert "Dog" == workbook["Info"]["B5"].value
64+
assert "Woof Woof" == workbook["Info"]["B6"].value
65+
assert "Plastic bone" == workbook["Toys"]["A7"].value
66+
assert "Oh Yes" == workbook["Toys"]["B7"].value
67+
assert "Tennis Ball" == workbook["Toys"]["A8"].value
68+
assert "No" == workbook["Toys"]["B8"].value
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from spreadsheetforms.util import json_set_deep_value
2+
3+
4+
def test_1():
5+
data = {}
6+
json_set_deep_value(data, "cat", "cool")
7+
assert data == {"cat": "cool"}
8+
9+
10+
def test_2():
11+
data = {}
12+
json_set_deep_value(data, "cat/name", "Bob")
13+
assert data == {"cat": {"name": "Bob"}}
14+
15+
16+
def test_3():
17+
data = {}
18+
json_set_deep_value(data, "cat/name/official", "Mr Bob The Magnificent")
19+
json_set_deep_value(data, "cat/name/informal", "Bob")
20+
assert data == {
21+
"cat": {"name": {"informal": "Bob", "official": "Mr Bob The Magnificent"}}
22+
}

0 commit comments

Comments
 (0)