flinventory 0.3.0__py3-none-any.whl
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.
- flinventory/__init__.py +8 -0
- flinventory/__main__.py +45 -0
- flinventory/box.py +289 -0
- flinventory/constant.py +285 -0
- flinventory/datacleanup.py +349 -0
- flinventory/defaulted_data.py +552 -0
- flinventory/generate_labels.py +214 -0
- flinventory/inventory_io.py +295 -0
- flinventory/location.py +455 -0
- flinventory/sign.py +160 -0
- flinventory/signprinter_latex.py +471 -0
- flinventory/thing.py +145 -0
- flinventory/thingtemplate_latex/.gitignore +6 -0
- flinventory/thingtemplate_latex/dummyImage.jpg +0 -0
- flinventory/thingtemplate_latex/sign.tex +26 -0
- flinventory/thingtemplate_latex/signlist-footer.tex +1 -0
- flinventory/thingtemplate_latex/signlist-header.tex +12 -0
- flinventory/thingtemplate_latex/signs-example.tex +95 -0
- flinventory-0.3.0.dist-info/METADATA +63 -0
- flinventory-0.3.0.dist-info/RECORD +23 -0
- flinventory-0.3.0.dist-info/WHEEL +4 -0
- flinventory-0.3.0.dist-info/entry_points.txt +4 -0
- flinventory-0.3.0.dist-info/licenses/LICENSE +626 -0
@@ -0,0 +1,349 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""Collection of function to clean up data.
|
3
|
+
|
4
|
+
Also used for storing code that might be useful
|
5
|
+
for cleaning data but is not in use right now."""
|
6
|
+
|
7
|
+
import argparse
|
8
|
+
import json
|
9
|
+
import os
|
10
|
+
import logging
|
11
|
+
|
12
|
+
import yaml
|
13
|
+
|
14
|
+
from . import constant
|
15
|
+
from . import inventory_io
|
16
|
+
from .box import BoxedThing
|
17
|
+
|
18
|
+
|
19
|
+
SHORTCUTS = {
|
20
|
+
"sq": (18, 10),
|
21
|
+
"sqtp": (6.5, 6.5),
|
22
|
+
"rectp": (8.5, 5.5),
|
23
|
+
"sb": (5.9, 1.6),
|
24
|
+
"es": (30, 10),
|
25
|
+
"ef": (9, 6.5),
|
26
|
+
"wd": (6, 2.1),
|
27
|
+
}
|
28
|
+
"""Shortcuts for sign sizes.
|
29
|
+
|
30
|
+
"s": "width height"
|
31
|
+
"s": "sq" -> square (bigger) cardboard box: 18 x 10
|
32
|
+
"s": "sqtp" -> square Tetrapak: 6.5 x 6.5
|
33
|
+
"s": "rectp" -> rectangular Tetrapak: 8.5 x 5.5
|
34
|
+
"s": "sb" -> Schäferbox mit Einsteckhalterung: 6.0x1.4 cm
|
35
|
+
"s": "es" -> Eurokiste lange Seite (groß): 30 x 10 cm
|
36
|
+
"s": "ef" -> Eurokiste kurze Seite (zum Einstecken): 9 x 6.5
|
37
|
+
"s": "wd" -> kleine weiße Döschen: 6 x 2.1 cm
|
38
|
+
"""
|
39
|
+
|
40
|
+
LOGGER = logging.getLogger(__name__)
|
41
|
+
|
42
|
+
|
43
|
+
def add_arg_parsers(subparsers):
|
44
|
+
"""Add command line options to support the datacleanup functionality.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
subparsers: object to add sub parsers to
|
48
|
+
"""
|
49
|
+
|
50
|
+
parser_example = subparsers.add_parser(
|
51
|
+
"example", description="This command does nothing"
|
52
|
+
)
|
53
|
+
parser_example.add_argument(
|
54
|
+
"--verbose", "-v", action="count", help="Also this option does nothing."
|
55
|
+
)
|
56
|
+
parser_example.set_defaults(func=example_action)
|
57
|
+
|
58
|
+
parser_normalize = subparsers.add_parser(
|
59
|
+
"normalize",
|
60
|
+
description=(
|
61
|
+
"read thing list and write it"
|
62
|
+
" so that further automatic"
|
63
|
+
" alterations make smaller diffs"
|
64
|
+
),
|
65
|
+
)
|
66
|
+
parser_normalize.set_defaults(func=normalize_thing_list)
|
67
|
+
|
68
|
+
parser_printed = subparsers.add_parser(
|
69
|
+
"printed",
|
70
|
+
description=(
|
71
|
+
'Add the "printed" attribute'
|
72
|
+
+ " to all things that have sign info so that they"
|
73
|
+
+ " are not printed next time."
|
74
|
+
),
|
75
|
+
)
|
76
|
+
parser_printed.set_defaults(func=add_printed)
|
77
|
+
|
78
|
+
parser_shortcutreplacement = subparsers.add_parser(
|
79
|
+
"shortcut",
|
80
|
+
description=(
|
81
|
+
"Replace shortcut 's': " + "'width height' by " + "appropriate values."
|
82
|
+
),
|
83
|
+
)
|
84
|
+
parser_shortcutreplacement.set_defaults(func=convert_shortcuts)
|
85
|
+
|
86
|
+
parser_unprinted = subparsers.add_parser(
|
87
|
+
"unprinted", description='Remove the "printed" attribute from all things.'
|
88
|
+
)
|
89
|
+
parser_unprinted.set_defaults(func=remove_printed)
|
90
|
+
|
91
|
+
parser_use_ids = subparsers.add_parser(
|
92
|
+
"useIDs", description="Replace names in thing references by their IDs."
|
93
|
+
)
|
94
|
+
parser_use_ids.set_defaults(func=use_ids)
|
95
|
+
|
96
|
+
parser_locations_file = subparsers.add_parser(
|
97
|
+
"locationsFile",
|
98
|
+
description="Add locations from a locations.yaml file to their things.",
|
99
|
+
)
|
100
|
+
parser_locations_file.set_defaults(func=convert_locations_file)
|
101
|
+
|
102
|
+
|
103
|
+
def example_action(args):
|
104
|
+
"""Does nothing.
|
105
|
+
|
106
|
+
Is an example for a function that is called via a subparser.
|
107
|
+
"""
|
108
|
+
LOGGER.debug(f"Example action: counted: {args.verbose}")
|
109
|
+
|
110
|
+
|
111
|
+
def normalize_thing_list(args):
|
112
|
+
"""Read thing list and write it again."""
|
113
|
+
things = inventory_io.Inventory.from_json_files(directory=args.dataDirectory)
|
114
|
+
things.save()
|
115
|
+
|
116
|
+
|
117
|
+
def use_ids(args):
|
118
|
+
"""Read thing list, replace references in 'part_of' to other things by their ids and write.
|
119
|
+
|
120
|
+
Note that if 'part_of' values are given by a default value (e.g. from wikidata data),
|
121
|
+
it is still "replaced" in the main thing. Maybe this is what we want, maybe not.
|
122
|
+
|
123
|
+
Only replace if ids for all elements of 'part_of' lists are found.
|
124
|
+
Otherwise, log warning.
|
125
|
+
"""
|
126
|
+
things = inventory_io.Inventory.from_json_files(directory=args.dataDirectory)
|
127
|
+
for thing in things:
|
128
|
+
try:
|
129
|
+
parents = thing["part_of"]
|
130
|
+
except KeyError:
|
131
|
+
continue
|
132
|
+
parent_ids = []
|
133
|
+
for parent_name in parents:
|
134
|
+
# first check if it is already an id
|
135
|
+
try:
|
136
|
+
things.get_by_id(parent_name)
|
137
|
+
except KeyError:
|
138
|
+
# expected
|
139
|
+
pass
|
140
|
+
else:
|
141
|
+
parent_ids.append(parent_name)
|
142
|
+
continue
|
143
|
+
# otherwise search by name:
|
144
|
+
possible_parents = list(things.get_by_best("name", parent_name))
|
145
|
+
if len(possible_parents) == 1:
|
146
|
+
parent_ids.append(things.get_id(possible_parents[0]))
|
147
|
+
elif len(possible_parents) == 0:
|
148
|
+
logging.getLogger("datacleanup: use_ids").warning(
|
149
|
+
f"Got no thing with name {parent_name}."
|
150
|
+
)
|
151
|
+
break
|
152
|
+
else:
|
153
|
+
logging.getLogger("datacleanup: use_ids").warning(
|
154
|
+
f"Got several things with name {parent_name}."
|
155
|
+
)
|
156
|
+
break
|
157
|
+
else:
|
158
|
+
# if no break was encountered, all parent_names could be matched
|
159
|
+
assert len(parent_ids) == len(parents), (
|
160
|
+
"No error was noticed but a parent got lost for "
|
161
|
+
f'{thing.best("name")} from {parents} to {parent_ids}.'
|
162
|
+
)
|
163
|
+
thing["part_of"] = parent_ids
|
164
|
+
print("Should all be saved, but do it again.")
|
165
|
+
things.save()
|
166
|
+
|
167
|
+
|
168
|
+
def add_printed(args):
|
169
|
+
"""Add to all things the attribute printed=True."""
|
170
|
+
|
171
|
+
def add_printed_once(thing: BoxedThing) -> None:
|
172
|
+
if thing.sign.should_be_printed():
|
173
|
+
thing.sign["printed"] = True
|
174
|
+
|
175
|
+
transform_all(args, add_printed_once)
|
176
|
+
|
177
|
+
|
178
|
+
def remove_printed(args):
|
179
|
+
"""Remove the attribute printed on all things."""
|
180
|
+
|
181
|
+
def remove(thing: BoxedThing):
|
182
|
+
try:
|
183
|
+
del thing.sign["printed"]
|
184
|
+
except KeyError:
|
185
|
+
pass
|
186
|
+
|
187
|
+
transform_all(args, remove)
|
188
|
+
|
189
|
+
|
190
|
+
def convert_shortcuts(args):
|
191
|
+
"""Convert the shortcuts to correct fields."""
|
192
|
+
|
193
|
+
def replace_shortcut(thing: BoxedThing) -> None:
|
194
|
+
try:
|
195
|
+
value = thing.sign["s"]
|
196
|
+
except KeyError:
|
197
|
+
return # no change
|
198
|
+
if value in SHORTCUTS:
|
199
|
+
thing.sign["width"] = SHORTCUTS[value][0]
|
200
|
+
thing.sign["height"] = SHORTCUTS[value][1]
|
201
|
+
del thing.sign["s"]
|
202
|
+
return
|
203
|
+
try:
|
204
|
+
width, height = thing.sign["s"].split()
|
205
|
+
except AttributeError:
|
206
|
+
LOGGER.warning(
|
207
|
+
f"Thing {thing.best('name', backup='?')} has attribute sign['s'] but it is not a string."
|
208
|
+
)
|
209
|
+
return
|
210
|
+
except ValueError:
|
211
|
+
LOGGER.warning(
|
212
|
+
f"Thing {thing.best('name')} has attribute sign['s'] "
|
213
|
+
f"but it is not exactly two items separated by space."
|
214
|
+
)
|
215
|
+
return
|
216
|
+
try:
|
217
|
+
width = int(width)
|
218
|
+
if width <= 0:
|
219
|
+
raise ValueError()
|
220
|
+
except ValueError:
|
221
|
+
try:
|
222
|
+
width = float(width)
|
223
|
+
if width <= 0:
|
224
|
+
raise ValueError() # pylint: disable=raise-missing-from
|
225
|
+
except ValueError:
|
226
|
+
LOGGER.warning(
|
227
|
+
f"width {width} in thing {thing.best("name")} is not a positive number"
|
228
|
+
)
|
229
|
+
return
|
230
|
+
try:
|
231
|
+
height = int(height)
|
232
|
+
if height <= 0:
|
233
|
+
raise ValueError()
|
234
|
+
except ValueError:
|
235
|
+
try:
|
236
|
+
height = float(height)
|
237
|
+
if height <= 0:
|
238
|
+
raise ValueError() # pylint: disable=raise-missing-from
|
239
|
+
except ValueError:
|
240
|
+
LOGGER.warning(
|
241
|
+
f"height {height} in thing {thing.best('name')} is not a positive number"
|
242
|
+
)
|
243
|
+
return
|
244
|
+
thing.sign["width"] = width
|
245
|
+
thing.sign["height"] = height
|
246
|
+
del thing.sign["s"]
|
247
|
+
assert "width" in thing.sign
|
248
|
+
assert "height" in thing.sign
|
249
|
+
assert "s" not in thing.sign
|
250
|
+
|
251
|
+
transform_all(args, replace_shortcut)
|
252
|
+
|
253
|
+
|
254
|
+
def convert_locations_file(args):
|
255
|
+
"""Reads a deprecated locations.yaml file and adds the locations to the things.
|
256
|
+
|
257
|
+
Assumes that the things are already split into one thing.yaml file per thing.
|
258
|
+
|
259
|
+
The locations file must be called `locations.json` or `locations.yaml`.
|
260
|
+
|
261
|
+
The schema in locations.json/yaml is dumped into the schema file if there is
|
262
|
+
no schema file yet.
|
263
|
+
"""
|
264
|
+
inventory = inventory_io.Inventory.from_json_files(args.dataDirectory)
|
265
|
+
try:
|
266
|
+
locations_file = open(os.path.join(args.dataDirectory, "locations.json"))
|
267
|
+
except FileNotFoundError:
|
268
|
+
try:
|
269
|
+
locations_file = open(os.path.join(args.dataDirectory, "locations.yaml"))
|
270
|
+
except FileNotFoundError:
|
271
|
+
print("No locations.json file found. Do nothing.")
|
272
|
+
return
|
273
|
+
else:
|
274
|
+
print("Use data from locations.yaml")
|
275
|
+
data = yaml.safe_load(locations_file)
|
276
|
+
else:
|
277
|
+
print("Use data from locations.json.")
|
278
|
+
data = json.load(locations_file)
|
279
|
+
|
280
|
+
for key, location_dict in data.get("part_locations", data["locations"]).items():
|
281
|
+
possible_things = list(inventory.get_by_best("name", key))
|
282
|
+
if len(possible_things) == 1:
|
283
|
+
thing = possible_things[0]
|
284
|
+
for loc_key, value in location_dict.items():
|
285
|
+
if loc_key.startswith("sign_"):
|
286
|
+
if loc_key.endswith("_de"):
|
287
|
+
thing.sign[loc_key[5:-3], "de"] = value
|
288
|
+
elif loc_key.endswith("_en"):
|
289
|
+
thing.sign[loc_key[5:-3], "en"] = value
|
290
|
+
else:
|
291
|
+
thing.sign[loc_key[5:]] = value
|
292
|
+
else:
|
293
|
+
thing.location[loc_key] = value
|
294
|
+
elif len(possible_things) == 0:
|
295
|
+
logging.error(f"No thing found with name {key}")
|
296
|
+
else:
|
297
|
+
logging.error(f"{len(possible_things)} found with name {key}.")
|
298
|
+
|
299
|
+
# inventory.save() # should not be necessary
|
300
|
+
|
301
|
+
if not os.path.exists(
|
302
|
+
schema_path := os.path.join(args.dataDirectory, constant.SCHEMA_FILE)
|
303
|
+
):
|
304
|
+
with open(schema_path, mode="w") as schema_file:
|
305
|
+
yaml.dump(data["schema"], schema_file, **constant.YAML_DUMP_OPTIONS)
|
306
|
+
|
307
|
+
|
308
|
+
def add_attribute_if(args, attribute, condition=lambda thing: True):
|
309
|
+
"""Add an attribute to all things that fulfill the condition.
|
310
|
+
|
311
|
+
Can only add attributes to thing, not location nor sign.
|
312
|
+
|
313
|
+
Attributes:
|
314
|
+
args: command line options including directory of inventory
|
315
|
+
attribute:
|
316
|
+
function thing -> to be added attribute
|
317
|
+
as (key, value) tuple
|
318
|
+
condition:
|
319
|
+
function thing -> bool: only add attribute if
|
320
|
+
condition(thing) is True
|
321
|
+
"""
|
322
|
+
|
323
|
+
def add_if(thing):
|
324
|
+
if condition(thing):
|
325
|
+
key, value = attribute(thing)
|
326
|
+
thing[key] = value
|
327
|
+
|
328
|
+
transform_all(args, add_if)
|
329
|
+
|
330
|
+
|
331
|
+
def transform_all(args, transformation):
|
332
|
+
"""Do something with all things: transformation."""
|
333
|
+
things = inventory_io.Inventory.from_json_files(directory=args.dataDirectory)
|
334
|
+
for thing in things:
|
335
|
+
transformation(thing)
|
336
|
+
things.save()
|
337
|
+
|
338
|
+
|
339
|
+
def logging_config(args):
|
340
|
+
"""Set the basic logging config."""
|
341
|
+
try:
|
342
|
+
os.mkdir(args.output_dir)
|
343
|
+
except FileExistsError:
|
344
|
+
pass # everything fine
|
345
|
+
logging.basicConfig(
|
346
|
+
filename=os.path.join(args.output_dir, args.logfile),
|
347
|
+
level=logging.DEBUG,
|
348
|
+
filemode="w",
|
349
|
+
)
|