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.
@@ -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
+ )