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
flinventory/location.py
ADDED
@@ -0,0 +1,455 @@
|
|
1
|
+
"""Locations as specified (by example) in locations.json.
|
2
|
+
|
3
|
+
The schema how locations are hierachically defined is defined in
|
4
|
+
locations.json.
|
5
|
+
|
6
|
+
Recognised keys are:
|
7
|
+
- seperator (str)
|
8
|
+
- schema (dict)
|
9
|
+
- name (str|int) : what this location is called
|
10
|
+
- shortcut (str|int) : how this location is called in a short location string
|
11
|
+
- levelname (str) : key for specifying subelements
|
12
|
+
- default (str) : name of default subelement. This is only used to help UIs to suggest
|
13
|
+
something. It is not used to fill missing data.
|
14
|
+
- subs (dict): same information for the subelements (a list of schemas)
|
15
|
+
if non-existing, all names are accepted
|
16
|
+
- subschema (dict): information given to all subelements
|
17
|
+
- locations (dict): where the things are
|
18
|
+
- keys (str) : given by levelname
|
19
|
+
- values (str|int): the possible values given by the keys in the
|
20
|
+
subs Schema or any value if no subs are given
|
21
|
+
"""
|
22
|
+
|
23
|
+
import logging
|
24
|
+
|
25
|
+
import pathlib
|
26
|
+
|
27
|
+
from typing import Any, Union, Optional, Self, Literal
|
28
|
+
import os.path
|
29
|
+
import yaml
|
30
|
+
|
31
|
+
from . import constant
|
32
|
+
|
33
|
+
Value = Union[str, int, float, bool]
|
34
|
+
"""Valid level specifier types (values in location dict)."""
|
35
|
+
|
36
|
+
|
37
|
+
class InvalidLocationSchema(Exception):
|
38
|
+
"""Raised if the schema is invalid.
|
39
|
+
|
40
|
+
Should be raised "from" the original exception.
|
41
|
+
"""
|
42
|
+
|
43
|
+
|
44
|
+
class InvalidLocation(Exception):
|
45
|
+
"""Raised if a location is invalid.
|
46
|
+
|
47
|
+
Should be raised "from" the original exception.
|
48
|
+
"""
|
49
|
+
|
50
|
+
|
51
|
+
class Schema:
|
52
|
+
"""Schema as described in module docstring, possibly with sub-schemas.
|
53
|
+
|
54
|
+
Not yet possible: creation of json based on Schema.
|
55
|
+
Do not know if I might need that.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self, json_content: dict[str, Any]):
|
59
|
+
"""Create schema with sub schemas."""
|
60
|
+
# just save it to be able to give it back
|
61
|
+
self.json = json_content
|
62
|
+
try:
|
63
|
+
self._name = json_content["name"]
|
64
|
+
except KeyError as key_error:
|
65
|
+
raise InvalidLocationSchema("Name is mandatory for schema.") from key_error
|
66
|
+
self._name = (
|
67
|
+
self._name.strip() if isinstance(self._name, str) else int(self._name)
|
68
|
+
)
|
69
|
+
if self._name == "":
|
70
|
+
raise InvalidLocationSchema(
|
71
|
+
"A schema name must not be the empty/ a pure whitespace string."
|
72
|
+
)
|
73
|
+
try:
|
74
|
+
self._shortcut = json_content["shortcut"]
|
75
|
+
except KeyError:
|
76
|
+
self._shortcut = (
|
77
|
+
self._name[0] if isinstance(self._name, str) else int(self._name)
|
78
|
+
)
|
79
|
+
self._levelname = json_content.get("levelname", None)
|
80
|
+
self._default = json_content.get("default", None)
|
81
|
+
if isinstance(self._default, str):
|
82
|
+
self._default = self._default.strip()
|
83
|
+
if self._default == "":
|
84
|
+
raise InvalidLocationSchema(
|
85
|
+
"A default subschema key must not be the empty string."
|
86
|
+
)
|
87
|
+
if self._levelname is None and self._default is not None:
|
88
|
+
raise InvalidLocationSchema(
|
89
|
+
"A default subschema makes no sense if no levelname is given."
|
90
|
+
)
|
91
|
+
self._sub_schema_default, self._subs = self._initialise_subs(json_content)
|
92
|
+
if self._levelname is None and self._subs is not None:
|
93
|
+
raise InvalidLocationSchema(
|
94
|
+
f"If sub schemas for {self._name} are mentioned, "
|
95
|
+
"also a levelname is necessary "
|
96
|
+
"to specify the subschema."
|
97
|
+
)
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def _initialise_subs(
|
101
|
+
cls, json_content: dict[str, Any]
|
102
|
+
) -> tuple[dict[str, Any], Optional[list[Self]]]:
|
103
|
+
"""Read sub element information and return info accordingly.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
- sub_schema_default
|
107
|
+
- subs
|
108
|
+
"""
|
109
|
+
sub_schema_default = json_content.get("subschema", {})
|
110
|
+
try:
|
111
|
+
if "name" in sub_schema_default or "shortcut" in sub_schema_default:
|
112
|
+
raise InvalidLocationSchema(
|
113
|
+
"It does not make sense to give all "
|
114
|
+
"sub elements the same name or shortcut."
|
115
|
+
)
|
116
|
+
except TypeError as typeerror:
|
117
|
+
# mainly check this here to make pylint happy with the "in" statements
|
118
|
+
raise InvalidLocationSchema("subschema must be a dict.") from typeerror
|
119
|
+
subs = None
|
120
|
+
try:
|
121
|
+
sub_element_list = json_content["subs"]
|
122
|
+
except KeyError:
|
123
|
+
pass
|
124
|
+
else:
|
125
|
+
subs = []
|
126
|
+
try:
|
127
|
+
sub_element_iter = iter(sub_element_list)
|
128
|
+
except TypeError as typeerror:
|
129
|
+
raise InvalidLocationSchema(
|
130
|
+
"subs contains something that is not a list"
|
131
|
+
) from typeerror
|
132
|
+
for sub_element in sub_element_iter:
|
133
|
+
if isinstance(sub_element, (str, int)):
|
134
|
+
subs.append(cls(sub_schema_default | {"name": sub_element}))
|
135
|
+
elif isinstance(sub_element, dict):
|
136
|
+
subs.append(cls(sub_schema_default | sub_element))
|
137
|
+
else:
|
138
|
+
raise InvalidLocationSchema(
|
139
|
+
'Elements of list in "subs" must be '
|
140
|
+
"str, int (name) or dict (schema)."
|
141
|
+
)
|
142
|
+
assert len(subs) == len(sub_element_list)
|
143
|
+
return sub_schema_default, subs
|
144
|
+
|
145
|
+
def get_subschema(self, key: str | int) -> Self:
|
146
|
+
"""Get the subschema for the given key.
|
147
|
+
|
148
|
+
If subs are given, first their names are checked for a match,
|
149
|
+
then their shortcuts.
|
150
|
+
Otherwise, a generic schema based on the default schema is given.
|
151
|
+
Then all keys are accepted.
|
152
|
+
"""
|
153
|
+
if self._subs is None:
|
154
|
+
return type(self)(self._sub_schema_default | {"name": key})
|
155
|
+
suitably_named_subs = [sub for sub in self._subs if sub.name == key]
|
156
|
+
if len(suitably_named_subs) == 1:
|
157
|
+
return suitably_named_subs[0]
|
158
|
+
if len(suitably_named_subs) > 1:
|
159
|
+
raise InvalidLocationSchema(
|
160
|
+
f"{self._levelname} has the name " f"{key} twice in subschemas."
|
161
|
+
)
|
162
|
+
suitably_named_subs = [sub for sub in self._subs if sub.shortcut == key]
|
163
|
+
if len(suitably_named_subs) == 1:
|
164
|
+
return suitably_named_subs[0]
|
165
|
+
if len(suitably_named_subs) > 1:
|
166
|
+
raise InvalidLocationSchema(
|
167
|
+
f"{self._levelname} has the shortcut " f"{key} twice in subschemas."
|
168
|
+
)
|
169
|
+
raise InvalidLocation(
|
170
|
+
f"The key {key} is not a valid subschema "
|
171
|
+
f"for {self._name} (levelname: {self._levelname}.)"
|
172
|
+
)
|
173
|
+
|
174
|
+
@property
|
175
|
+
def name(self) -> str | int:
|
176
|
+
"""Get the shortcut used in location strings."""
|
177
|
+
return self._name
|
178
|
+
|
179
|
+
@property
|
180
|
+
def shortcut(self) -> str | int:
|
181
|
+
"""Get the shortcut used in location strings."""
|
182
|
+
return self._shortcut
|
183
|
+
|
184
|
+
@property
|
185
|
+
def levelname(self) -> str:
|
186
|
+
"""Get levelname, which is the key in locations to specify the subschema.
|
187
|
+
|
188
|
+
Raises:
|
189
|
+
InvalidLocation if no subschemas are allowed due to missing levelname.
|
190
|
+
"""
|
191
|
+
if self._levelname is None:
|
192
|
+
raise InvalidLocation(f"The schema {self._name} has no levelname.")
|
193
|
+
return self._levelname
|
194
|
+
|
195
|
+
def get_schema_hierarchy(self, location_info: dict[str, Any]) -> list[Self]:
|
196
|
+
"""Return a list of nested schemas for this location starting with this.
|
197
|
+
|
198
|
+
Default values are ignored.
|
199
|
+
"""
|
200
|
+
if (
|
201
|
+
self._levelname is None # this schema has no subelements
|
202
|
+
# no sub is given and no default is set
|
203
|
+
or self._levelname not in location_info
|
204
|
+
# sub is explicitly set to None
|
205
|
+
or (
|
206
|
+
self._levelname in location_info
|
207
|
+
and location_info[self._levelname] is None
|
208
|
+
)
|
209
|
+
):
|
210
|
+
return [self]
|
211
|
+
return [self] + self.get_subschema(
|
212
|
+
location_info[self._levelname]
|
213
|
+
).get_schema_hierarchy(location_info)
|
214
|
+
|
215
|
+
def get_valid_subs(
|
216
|
+
self, shortcuts: Literal["yes", "only", "()", "*"] = "yes"
|
217
|
+
) -> Optional[list[Union[str, int]]]:
|
218
|
+
"""A list of valid sub schemas. Usable for suggesting options.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
shortcuts: if valid shortcuts should be listed as well:
|
222
|
+
'yes': shortcuts listed in the same way as names,
|
223
|
+
'only': only shortcuts are listed
|
224
|
+
'()': shortcuts are listed in parentheses after the name (note: result
|
225
|
+
elements are not valid values)
|
226
|
+
'*': shortcuts are marked with ** ** if they are in the name, otherwise like '()'
|
227
|
+
Return:
|
228
|
+
None if no levelname is given,
|
229
|
+
an empty list if everything is valid
|
230
|
+
"""
|
231
|
+
if self._levelname is None:
|
232
|
+
return None
|
233
|
+
if self._subs is None:
|
234
|
+
return []
|
235
|
+
if shortcuts == "yes":
|
236
|
+
return [name for sub in self._subs for name in (sub.name, sub.shortcut)]
|
237
|
+
if shortcuts == "only":
|
238
|
+
return [sub.shortcut for sub in self._subs]
|
239
|
+
if shortcuts == "()":
|
240
|
+
return [f"{sub.name} ({sub.shortcut})" for sub in self._subs]
|
241
|
+
if shortcuts == "*":
|
242
|
+
return [
|
243
|
+
(
|
244
|
+
(
|
245
|
+
f"{str(sub.name)[:str(sub.name).index(str(sub.shortcut))]}"
|
246
|
+
f"**{sub.shortcut}**"
|
247
|
+
+ str(sub.name)[
|
248
|
+
str(sub.name).index(str(sub.shortcut))
|
249
|
+
+ len(str(sub.shortcut)) :
|
250
|
+
]
|
251
|
+
)
|
252
|
+
if str(sub.shortcut) in str(sub.name)
|
253
|
+
else f"{sub.name} ({sub.shortcut})"
|
254
|
+
)
|
255
|
+
for sub in self._subs
|
256
|
+
]
|
257
|
+
raise ValueError('shortcuts needs to be one of "yes", "only", "()", "*"')
|
258
|
+
|
259
|
+
@classmethod
|
260
|
+
def from_file(cls, directory):
|
261
|
+
"""Read schema from SCHEMA_FILE in directory.
|
262
|
+
|
263
|
+
If file does not exist, create empty schema.
|
264
|
+
"""
|
265
|
+
try:
|
266
|
+
schema_file = open(os.path.join(directory, constant.SCHEMA_FILE))
|
267
|
+
except FileNotFoundError:
|
268
|
+
return cls({"name": "Workshop"})
|
269
|
+
return cls(yaml.safe_load(schema_file))
|
270
|
+
|
271
|
+
|
272
|
+
class Location(dict[str, Value]):
|
273
|
+
"""A location for stuff. Member var definition in schema.
|
274
|
+
|
275
|
+
Values are accessed with dict member notation. `loc["shelf"] = 5`
|
276
|
+
|
277
|
+
Attributes:
|
278
|
+
_schema: schema defining the meaning of the keys
|
279
|
+
_levels: list of schemas for all levels of the location hierarchy
|
280
|
+
options: global options including the seperator
|
281
|
+
"""
|
282
|
+
|
283
|
+
def __init__(
|
284
|
+
self,
|
285
|
+
schema: Schema,
|
286
|
+
data: dict[str, Value],
|
287
|
+
directory: str,
|
288
|
+
options: constant.Options,
|
289
|
+
):
|
290
|
+
"""Create a location from data and the schema.
|
291
|
+
|
292
|
+
schema: location schema
|
293
|
+
data: location data as a dict
|
294
|
+
directory: where to save this location to (saved to directory/{constant.LOCATION_FILE})
|
295
|
+
options: general inventory options (separator is used)
|
296
|
+
"""
|
297
|
+
self._directory = None # no autosave during initialisation
|
298
|
+
super().__init__(data)
|
299
|
+
self.options = options
|
300
|
+
self._directory = directory
|
301
|
+
self._logger = logging.getLogger(__name__)
|
302
|
+
self._schema = schema
|
303
|
+
self._levels = self._schema.get_schema_hierarchy(self)
|
304
|
+
self.canonicalize()
|
305
|
+
|
306
|
+
def _shortcut(self):
|
307
|
+
"""Get string representation with shortcuts."""
|
308
|
+
return self.options.separator.join(
|
309
|
+
str(level.shortcut) for level in self._levels
|
310
|
+
)
|
311
|
+
|
312
|
+
@property
|
313
|
+
def long_name(self):
|
314
|
+
"""Get the string representation with names."""
|
315
|
+
return self.options.separator.join(str(level.name) for level in self._levels)
|
316
|
+
|
317
|
+
@property
|
318
|
+
def schema(self) -> Schema:
|
319
|
+
"""Return the schema given in the location file used to define this location.
|
320
|
+
|
321
|
+
Please do not alter.
|
322
|
+
"""
|
323
|
+
return self._schema
|
324
|
+
|
325
|
+
def __str__(self):
|
326
|
+
"""Get string representation of the location.
|
327
|
+
|
328
|
+
Uses the schema and the seperator.
|
329
|
+
|
330
|
+
Assumes that the schema is valid.
|
331
|
+
|
332
|
+
Raises:
|
333
|
+
InvalidLocation: if the location info do not suit
|
334
|
+
the schema
|
335
|
+
Returns:
|
336
|
+
shortcuts for places separated by
|
337
|
+
seperator given at initialisation
|
338
|
+
Can be an empty string if the top-level schema
|
339
|
+
is not present or None.
|
340
|
+
"""
|
341
|
+
return self._shortcut()
|
342
|
+
|
343
|
+
def to_sortable_tuple(self):
|
344
|
+
"""Return the data of this location as sortable tuple."""
|
345
|
+
return (level.name for level in self._levels)
|
346
|
+
|
347
|
+
def canonicalize(self):
|
348
|
+
"""Replace shortcuts by entire names."""
|
349
|
+
for index, level in list(enumerate(self._levels))[:-1]:
|
350
|
+
# use super() because with self[key] = ... we get an infinite recursion:
|
351
|
+
super().__setitem__(level.levelname, self._levels[index + 1].name)
|
352
|
+
|
353
|
+
def __setitem__(self, level: str, value: Value) -> None:
|
354
|
+
"""Set one level info.
|
355
|
+
|
356
|
+
Or deletes it if it's None or "" or [] or {}
|
357
|
+
|
358
|
+
Update internal info.
|
359
|
+
"""
|
360
|
+
if value in [None, "", [], {}]:
|
361
|
+
if level in self:
|
362
|
+
del self[level]
|
363
|
+
else:
|
364
|
+
super().__setitem__(level, value)
|
365
|
+
self._levels = self._schema.get_schema_hierarchy(self)
|
366
|
+
if self._directory is not None:
|
367
|
+
self.save()
|
368
|
+
|
369
|
+
def __delitem__(self, key: str) -> None:
|
370
|
+
"""Deletes one information.
|
371
|
+
|
372
|
+
Update internal info.
|
373
|
+
"""
|
374
|
+
super().__delitem__(key)
|
375
|
+
self._levels = self._schema.get_schema_hierarchy(self)
|
376
|
+
if self._directory is not None:
|
377
|
+
self.save()
|
378
|
+
|
379
|
+
def __bool__(self):
|
380
|
+
"""True if something is saved."""
|
381
|
+
return bool(super())
|
382
|
+
|
383
|
+
def save(self):
|
384
|
+
"""Save location data to yaml file."""
|
385
|
+
path = pathlib.Path(os.path.join(self._directory, constant.LOCATION_FILE))
|
386
|
+
if len(self) > 0: # not empty?
|
387
|
+
with open(path, mode="w", encoding="utf-8") as location_file:
|
388
|
+
yaml.dump(
|
389
|
+
data=self.to_jsonable_data(),
|
390
|
+
stream=location_file,
|
391
|
+
**constant.YAML_DUMP_OPTIONS,
|
392
|
+
)
|
393
|
+
self._logger.info(f"Saved {self.directory} location to {path}.")
|
394
|
+
else:
|
395
|
+
if path.is_file():
|
396
|
+
self._logger.info(f"Delete {path}")
|
397
|
+
path.unlink(missing_ok=True)
|
398
|
+
|
399
|
+
def to_jsonable_data(self) -> dict[str, Value]:
|
400
|
+
"""Convert to dict with levelnames as keys.
|
401
|
+
|
402
|
+
Sorts by schema hierarchy.
|
403
|
+
"""
|
404
|
+
self._levels = self.schema.get_schema_hierarchy(self)
|
405
|
+
as_dict = {
|
406
|
+
level.levelname: self._levels[index + 1].name
|
407
|
+
for index, level in list(enumerate(self._levels))[:-1]
|
408
|
+
}
|
409
|
+
for key in self:
|
410
|
+
as_dict.setdefault(key, self[key])
|
411
|
+
return as_dict
|
412
|
+
|
413
|
+
@classmethod
|
414
|
+
def from_yaml_file(cls, directory: str, schema: Schema, options: constant.Options):
|
415
|
+
"""Create location from yaml file.
|
416
|
+
|
417
|
+
Args:
|
418
|
+
directory: file is directory/{constant.LOCATION_FILE}
|
419
|
+
schema: location schema for interpreting the data
|
420
|
+
options: inventory-wide options, in particular separator
|
421
|
+
"""
|
422
|
+
path = pathlib.Path(os.path.join(directory, constant.LOCATION_FILE))
|
423
|
+
try:
|
424
|
+
location_file = open(path, mode="r", encoding="utf-8")
|
425
|
+
except FileNotFoundError:
|
426
|
+
return cls(schema=schema, data={}, directory=directory, options=options)
|
427
|
+
with location_file:
|
428
|
+
data = yaml.safe_load(location_file)
|
429
|
+
return cls(
|
430
|
+
schema=schema,
|
431
|
+
data=data,
|
432
|
+
directory=directory,
|
433
|
+
options=options,
|
434
|
+
)
|
435
|
+
|
436
|
+
@property
|
437
|
+
def directory(self) -> Optional[str]:
|
438
|
+
"""The directory in which the location data file is saved.
|
439
|
+
|
440
|
+
If none, changes are not saved.
|
441
|
+
"""
|
442
|
+
return self._directory
|
443
|
+
|
444
|
+
@directory.setter
|
445
|
+
def directory(self, new_directory):
|
446
|
+
"""Set the directory in which the data file is saved.
|
447
|
+
|
448
|
+
Save the data there.
|
449
|
+
The old data file is removed.
|
450
|
+
"""
|
451
|
+
pathlib.Path(os.path.join(self._directory, constant.LOCATION_FILE)).unlink(
|
452
|
+
missing_ok=True
|
453
|
+
)
|
454
|
+
self._directory = new_directory
|
455
|
+
self.save()
|
flinventory/sign.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
#! /usr/bin/env python
|
2
|
+
"""Class sign that contains the info for a sign for a thing."""
|
3
|
+
import itertools
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os.path
|
7
|
+
|
8
|
+
import pathlib
|
9
|
+
import yaml
|
10
|
+
from typing import Optional
|
11
|
+
from typing_extensions import override
|
12
|
+
|
13
|
+
from . import constant
|
14
|
+
from . import defaulted_data
|
15
|
+
|
16
|
+
|
17
|
+
class Sign(defaulted_data.DefaultedDict):
|
18
|
+
"""Contains info about a sign."""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
data: defaulted_data.Data,
|
23
|
+
thing: defaulted_data.DefaultedDict,
|
24
|
+
options: constant.Options,
|
25
|
+
directory: Optional[str],
|
26
|
+
):
|
27
|
+
"""Create a sign.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
data: dict with data, e.g. read from yaml file
|
31
|
+
thing: similar structured dict object from which to use data if
|
32
|
+
it is not overwritten in data. Useful for the name which is usually
|
33
|
+
given by the thing for which this is a sign but can be replaced by a
|
34
|
+
different version of the name in data
|
35
|
+
options: languages and other inventory-wide options
|
36
|
+
directory: directory where to save this sign data when changed.
|
37
|
+
The file where the data is saved is directory/{constant.SIGN_FILE}
|
38
|
+
If None, it not saved. (Can be set later to save.)
|
39
|
+
"""
|
40
|
+
self._directory = None # to avoid saving during initialisation
|
41
|
+
for source, target in [("fontsize_de", ("fontsize", "de")),
|
42
|
+
("fontsize_en", ("fontsize", "en")),
|
43
|
+
("fontsize_main", ("fontsize", "de")),
|
44
|
+
("fontsize_secondary", ("fontsize", "en"))]:
|
45
|
+
try:
|
46
|
+
data[target] = data[source]
|
47
|
+
del data[source]
|
48
|
+
# could throw exception if source was in default but probably will never happen
|
49
|
+
except KeyError:
|
50
|
+
pass # fine, not there
|
51
|
+
super().__init__(
|
52
|
+
data=data,
|
53
|
+
default=thing,
|
54
|
+
non_defaulted=["width", "height", "printed", "landscape", "fontsize"],
|
55
|
+
lists=thing.lists,
|
56
|
+
translated=thing.translated + ("fontsize",),
|
57
|
+
default_order=itertools.chain(
|
58
|
+
thing.default_order,
|
59
|
+
(
|
60
|
+
"width",
|
61
|
+
"height",
|
62
|
+
"location_shift_down",
|
63
|
+
"fontsize",
|
64
|
+
"printed",
|
65
|
+
),
|
66
|
+
),
|
67
|
+
options=options,
|
68
|
+
)
|
69
|
+
self._logger = logging.getLogger(__name__)
|
70
|
+
self._directory = directory
|
71
|
+
|
72
|
+
@property
|
73
|
+
def directory(self) -> Optional[str]:
|
74
|
+
"""The directory in which the sign data file is saved.
|
75
|
+
|
76
|
+
None if data is not saved.
|
77
|
+
"""
|
78
|
+
return self._directory
|
79
|
+
|
80
|
+
@directory.setter
|
81
|
+
def directory(self, new_directory: Optional[str]):
|
82
|
+
"""Set the directory in which the data file is saved.
|
83
|
+
|
84
|
+
Save the data there if it is not None.
|
85
|
+
The old data file is removed.
|
86
|
+
"""
|
87
|
+
pathlib.Path(os.path.join(self._directory, constant.SIGN_FILE)).unlink(
|
88
|
+
missing_ok=True
|
89
|
+
)
|
90
|
+
self._directory = new_directory
|
91
|
+
self.save()
|
92
|
+
|
93
|
+
def printable(self) -> bool:
|
94
|
+
"""True if width and height are given."""
|
95
|
+
return "width" in self and "height" in self
|
96
|
+
|
97
|
+
def should_be_printed(self) -> bool:
|
98
|
+
"""True if printable and not printed."""
|
99
|
+
return self.printable() and not bool(self.get("printed", False))
|
100
|
+
|
101
|
+
def save(self):
|
102
|
+
"""Save data to yaml file.
|
103
|
+
|
104
|
+
Do not check if something is overwritten.
|
105
|
+
|
106
|
+
Delete file if no data is there to be written.
|
107
|
+
|
108
|
+
If self.directory is None, do nothing.
|
109
|
+
Todo: include git add and commit.
|
110
|
+
"""
|
111
|
+
if self._directory is None:
|
112
|
+
return
|
113
|
+
jsonable_data = self.to_jsonable_data()
|
114
|
+
path = pathlib.Path(os.path.join(self._directory, constant.SIGN_FILE))
|
115
|
+
if not jsonable_data:
|
116
|
+
if path.is_file():
|
117
|
+
self._logger.info(f"Delete {path}")
|
118
|
+
path.unlink(missing_ok=True)
|
119
|
+
else:
|
120
|
+
with open(path, mode="w", encoding="utf-8") as sign_file:
|
121
|
+
yaml.dump(
|
122
|
+
self.to_jsonable_data(), sign_file, **constant.YAML_DUMP_OPTIONS
|
123
|
+
)
|
124
|
+
self._logger.info(
|
125
|
+
f"Saved {self.get(('name', 0), self._directory)} sign to {path}."
|
126
|
+
)
|
127
|
+
|
128
|
+
@override
|
129
|
+
def __setitem__(self, key, value):
|
130
|
+
"""self[key] = value as in DefaultedDict but saved afterward."""
|
131
|
+
super().__setitem__(key, value)
|
132
|
+
self.save()
|
133
|
+
|
134
|
+
@override
|
135
|
+
def __delitem__(self, key):
|
136
|
+
"""del self[key] as in DefaultedDict but saved afterward."""
|
137
|
+
super().__delitem__(key)
|
138
|
+
self.save()
|
139
|
+
|
140
|
+
@classmethod
|
141
|
+
def from_yaml_file(
|
142
|
+
cls,
|
143
|
+
directory: str,
|
144
|
+
thing: defaulted_data.DefaultedDict,
|
145
|
+
options: constant.Options,
|
146
|
+
):
|
147
|
+
"""Create a sign from data in a file.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
directory: directory with sign file
|
151
|
+
thing: thing for which this is a sign. Used as default data.
|
152
|
+
options: inventory-wide options
|
153
|
+
"""
|
154
|
+
path = pathlib.Path(os.path.join(directory, constant.SIGN_FILE))
|
155
|
+
try:
|
156
|
+
sign_file = open(path, mode="r", encoding="utf-8")
|
157
|
+
except FileNotFoundError:
|
158
|
+
return cls({}, thing, options, directory)
|
159
|
+
with sign_file:
|
160
|
+
return cls(yaml.safe_load(sign_file), thing, options, directory)
|