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,471 @@
1
+ #!/usr/bin/env python3
2
+ """Create a latex document with signs (For printing and glueing to boxes).
3
+
4
+ Todo:
5
+ rename font size to text scale. Because it is used to scale instead of setting
6
+ a font size. So the 'normal' is not 12 (pt) but 1.
7
+ """
8
+ import importlib.resources
9
+ import os.path
10
+ import logging
11
+ import subprocess
12
+ from typing import Union, Iterable
13
+
14
+ from .box import BoxedThing
15
+ from .sign import Sign
16
+
17
+ from . import thingtemplate_latex
18
+
19
+ TEMPLATE_PACKAGE = thingtemplate_latex
20
+ TEMPLATE_PRE = "signlist-header.tex"
21
+ #importlib.resources.read_text("flinventory", "signlist-header.tex"))
22
+ TEMPLATE_POST = "signlist-footer.tex"
23
+ TEMPLATE_SIGN = "sign.tex"
24
+ DUMMY_IMAGE = "dummyImage.jpg"
25
+ AUX_DIR = "latexAux"
26
+
27
+ UNIT = "cm" # todo: replace by length_unit in options, includes scaling the constants
28
+ # estimated by one example word
29
+ # at standard fontsize german
30
+ STANDARD_LETTER_LENGTH = 0.2 # in cm at font size 12pt
31
+ STANDARD_LETTER_HEIGHT = (12 + 1) * 0.03515 # in cm at font size 12 + linesep
32
+ # weird number is size of pt in LaTeX in cm
33
+ STANDARD_FONT_SIZE_GERMAN = 1
34
+ STANDARD_FONTSIZE_ENGLISH = 0.8
35
+ # amount of space the location string is shifted down
36
+ # (usually negative amount to shift up)
37
+ STANDARD_LOCATION_SHIFT_DOWN = r"-.8\baselineskip"
38
+ # in symbols:
39
+ STANDARD_LINE_LENGTH = 15
40
+ # how much bigger german should roughly be than english
41
+ GERMAN_TO_ENGLISH_SHARE = 1.25
42
+ # how big the image should be in parts of entire sign (in portrait mode)
43
+ IMAGE_SHARE = 0.5
44
+ # how much of the sign height can actually be used for text (due to margins)
45
+ # (in landscape mode)
46
+ TEXT_SHARE = 0.7
47
+ # minimum ration width/height for using landscape mode
48
+ LANDSCAPE_MIN_RATIO = 2
49
+ # paper text width (a4 = 21 cm wide) in cm
50
+ PAPER_TEXT_WIDTH = 20
51
+ # minimal height of sign for including an image (in cm)
52
+ MINIMAL_HEIGHT_FOR_IMAGE = 3
53
+ STANDARD_WIDTH = 8
54
+ # width of signs without given width [cm]
55
+ STANDARD_HEIGHT = 8
56
+ # height of signs without given height [cm]
57
+ MAX_WIDTH = 18 # otherwise too wide for A4 page
58
+ # maximum width of a sign [cm]
59
+ MAX_HEIGHT = 28 # otherwise too long for A4 page
60
+ # maximum height for a sign [cm]
61
+
62
+ def sanitize_latex_code(latex: Union[str, int, float]):
63
+ """Make a string insertable into LaTeX code.
64
+
65
+ Replace &, \\, ....
66
+ """
67
+ try:
68
+ for orig, new in [
69
+ ("\\", "\\textbackslash{}"),
70
+ ("{", "\\{"),
71
+ ("}", "\\}"),
72
+ ("$", "\\$"),
73
+ ("&", "\\&"),
74
+ ("#", "\\#"),
75
+ ("^", ""), # better: → \textasciicircum{} (requires the textcomp package)
76
+ ("_", "\\_"),
77
+ ("~", "\\textasciitilde{}"),
78
+ ("%", "\\%"),
79
+ ("<", "\\textless{}"),
80
+ (">", "\\textgreater{}"),
81
+ ("|", "\\textbar{}"),
82
+ ]:
83
+ latex = latex.replace(orig, new)
84
+ except AttributeError:
85
+ pass
86
+ return latex
87
+
88
+
89
+ class SignPrinterLaTeX:
90
+ # pylint: disable=too-many-instance-attributes
91
+ """Class encapsulating algorithm for creating printable signs from thing list.
92
+
93
+ Bad design choice: you have to create an object but actually this object is never used,
94
+ could all just be module or class functions.
95
+ """
96
+
97
+ def __init__(self, paths):
98
+ """Create SignPrinter by reading in template files."""
99
+ # pylint: disable-next=unbalanced-tuple-unpacking
100
+ self.pre, self.sign, self.post = self._read_templates()
101
+ self.output_dir = paths.output_dir
102
+ self.signs_file = paths.output_signs_latex
103
+ self.logger = logging.getLogger(__name__)
104
+
105
+ @staticmethod
106
+ def _read_templates() -> list[str]:
107
+ """Read the templates from the files.
108
+
109
+ Returns:
110
+ (iterable) main template, css header,
111
+ single size sign html file, double size sign html file
112
+ """
113
+ # todo remove function by using importlib, which gives directly the text
114
+ file_contents = []
115
+ for filename in [TEMPLATE_PRE, TEMPLATE_SIGN, TEMPLATE_POST]:
116
+ file_contents.append(importlib.resources.read_text(TEMPLATE_PACKAGE, filename))
117
+ return file_contents
118
+
119
+ def controlled_length(self, sign:Sign, key: str, backup: Union[int, float],
120
+ max: Union[int, float]):
121
+ """Get the value for the key if it exists and is a number and < max, otherwise backup."""
122
+ value = sign.get(key, backup)
123
+ try:
124
+ value = float(value)
125
+ except ValueError:
126
+ self.logger(f"{key} {value} is not a number. Use {backup} {UNIT} instead.")
127
+ return backup
128
+ if value <= 0:
129
+ self.logger(f"{key} {value} is non-positive and therefore not a valid sign {key}. "
130
+ f"Use {backup} {UNIT} instead.")
131
+ return backup
132
+ if value == int(value):
133
+ return min(int(value), max)
134
+ return min(value, max)
135
+
136
+ def width(self, thing: BoxedThing) -> Union[int, float]:
137
+ """Get the width of a sign, using backup and max size.
138
+
139
+ In UNIT.
140
+ """
141
+ return self.controlled_length(thing.sign, key='width', backup=STANDARD_WIDTH, max=MAX_WIDTH)
142
+
143
+ def height(self, thing: BoxedThing) -> Union[int, float]:
144
+ """Get the height of the sign of a boxed thing, using backup and max size."""
145
+ return self.controlled_length(thing.sign, key='height', backup=STANDARD_HEIGHT, max=MAX_HEIGHT)
146
+
147
+ def if_use_landscape_template(self, thing: BoxedThing):
148
+ """Return if we want to use the landscape template for this thing.
149
+
150
+ The landscape template has the image to the right of the text
151
+ instead of below it. So a portrait sign is usually still wider
152
+ than high.
153
+ """
154
+ try:
155
+ return thing.sign.get('landscape')
156
+ except KeyError:
157
+ try:
158
+ return self.width(thing) >= self.width(thing) * LANDSCAPE_MIN_RATIO
159
+ except KeyError:
160
+ return True
161
+
162
+ def guess_font_size(self, thing):
163
+ """Guess good font size.
164
+
165
+ Based on the length of the name and the size of the sign.
166
+
167
+ Returns:
168
+ guessed font size primary language, guessed font size secondary language in UNIT
169
+ """
170
+ # at first we do not support landscape signs
171
+ # if self.if_use_landscape_template(thing):
172
+ # return self.guess_font_size_landscape(thing)
173
+ return self.guess_font_size_portrait(thing)
174
+
175
+ @staticmethod
176
+ def guess_font_size_landscape(thing):
177
+ """Guess good font sizes for the landscape template."""
178
+ assert False, "landscape latex signs are not supperted"
179
+ # for simplicity assume only one line
180
+ width = self.width(thing)
181
+ height = self.height(thing)
182
+ used_width = width - height # that is approximately the part the image uses
183
+ german = thing.sign.get(('name', 0), '')
184
+ english = thing.sign.get(('name', 1), '')
185
+ german_expected_width_at_standard = len(german) * STANDARD_LETTER_LENGTH
186
+ german_max_by_width = (
187
+ STANDARD_FONT_SIZE_GERMAN * used_width / german_expected_width_at_standard
188
+ )
189
+ german_max_by_height = (
190
+ height
191
+ * TEXT_SHARE
192
+ / (GERMAN_TO_ENGLISH_SHARE + 1)
193
+ * GERMAN_TO_ENGLISH_SHARE
194
+ )
195
+ english_expected_width_at_standard = (
196
+ len(english)
197
+ * STANDARD_LETTER_LENGTH
198
+ * STANDARD_FONTSIZE_ENGLISH
199
+ / STANDARD_FONT_SIZE_GERMAN
200
+ )
201
+ english_max_by_width = (
202
+ STANDARD_FONTSIZE_ENGLISH * used_width / english_expected_width_at_standard
203
+ )
204
+ english_max_by_height = height * TEXT_SHARE / (GERMAN_TO_ENGLISH_SHARE + 1)
205
+ return (
206
+ min(german_max_by_width, german_max_by_height),
207
+ min(english_max_by_width, english_max_by_height),
208
+ )
209
+
210
+ def guess_font_size_portrait(self, thing: BoxedThing):
211
+ # pylint: disable=too-many-locals
212
+ """Guess what a good font size is for this sign.
213
+
214
+ Based on the length of the name and the size of the sign.
215
+
216
+ Returns:
217
+ guessed font size primary, guessed font size secondary in UNIT
218
+ """
219
+ # use german and english as aliases for primary and secondary language
220
+ # because it's easier to read
221
+ german = thing.get(('name', 0), '')
222
+ english = thing.get(('name', 1), '')
223
+ german_words = german.replace("-", "- ").split() or [" "]
224
+ english_words = english.replace("-", " ").split() or [" "]
225
+ # ignore cases with more than 2 lines, should be considered by hand
226
+ max_font_sizes = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
227
+ # there are 4 line number cases, see below in for loop
228
+ GERMAN_INDEX = 0 # pylint: disable=invalid-name
229
+ ENGLISH_INDEX = 1 # pylint: disable=invalid-name
230
+ SUM_INDEX = 2 # pylint: disable=invalid-name
231
+ # self.logger.debug("g: {}; e: {}, width: {}, heigth: {}".format(
232
+ # german, english, *thing.sign.size # unpacking pair
233
+ # ))
234
+ width = self.width(thing)
235
+ height = self.width(thing)
236
+ for german_number_lines, english_number_lines, case in [
237
+ (1, 1, 0),
238
+ (1, 2, 1),
239
+ (2, 1, 2),
240
+ (2, 2, 3),
241
+ ]:
242
+ german_length = max(
243
+ (
244
+ max(len(word) for word in german_words)
245
+ if german_number_lines == 2
246
+ else len(german)
247
+ ),
248
+ 1,
249
+ )
250
+ english_length = max(
251
+ (
252
+ max(len(word) for word in english_words)
253
+ if english_number_lines == 2
254
+ else len(english)
255
+ ),
256
+ 1,
257
+ )
258
+ german_expected_width_at_standard = german_length * STANDARD_LETTER_LENGTH
259
+ german_max_by_width = (
260
+ STANDARD_FONT_SIZE_GERMAN * width / german_expected_width_at_standard
261
+ )
262
+ text_height = height * (
263
+ 1 if height < MINIMAL_HEIGHT_FOR_IMAGE else IMAGE_SHARE
264
+ )
265
+ german_max_by_height = (
266
+ STANDARD_FONT_SIZE_GERMAN
267
+ * text_height
268
+ / STANDARD_LETTER_HEIGHT
269
+ / (german_number_lines * GERMAN_TO_ENGLISH_SHARE + english_number_lines)
270
+ * GERMAN_TO_ENGLISH_SHARE
271
+ )
272
+ english_expected_width_at_standard = (
273
+ english_length
274
+ * STANDARD_LETTER_LENGTH
275
+ * STANDARD_FONTSIZE_ENGLISH
276
+ / STANDARD_FONT_SIZE_GERMAN
277
+ )
278
+ # in factor size compared to normal size
279
+ english_max_by_width = (
280
+ STANDARD_FONTSIZE_ENGLISH * width / english_expected_width_at_standard
281
+ )
282
+ # in cm:
283
+ english_max_by_height = (
284
+ STANDARD_FONTSIZE_ENGLISH
285
+ * text_height
286
+ / STANDARD_LETTER_HEIGHT
287
+ / (german_number_lines * GERMAN_TO_ENGLISH_SHARE + english_number_lines)
288
+ )
289
+ logging.info(
290
+ f"german: {german}, english: {english}, case: {case} "
291
+ f"lines: ({german_number_lines}, {english_number_lines}, {case},"
292
+ f"german_max_by_height: {german_max_by_height}, "
293
+ f"german_max_by_width: {german_max_by_width}, "
294
+ f"english_max_by_height: {english_max_by_height}, "
295
+ f"english_max_by_width: {english_max_by_width}"
296
+ )
297
+ max_font_sizes[case][GERMAN_INDEX] = min(
298
+ [german_max_by_height, german_max_by_width]
299
+ )
300
+ max_font_sizes[case][ENGLISH_INDEX] = min(
301
+ [english_max_by_height, english_max_by_width]
302
+ )
303
+ max_font_sizes[case][SUM_INDEX] = (
304
+ max_font_sizes[case][GERMAN_INDEX] + max_font_sizes[case][ENGLISH_INDEX]
305
+ )
306
+ # self.logger.debug(
307
+ # "case: {}; gmaxH: {:.3f}; gmaxW: {:.3f}; emaxH: {:.3f}; emaxW: {:.3f}".format(
308
+ # case, german_max_by_height, german_max_by_width,
309
+ # english_max_by_height, english_max_by_width
310
+ # )
311
+ # )
312
+ german_max, english_max, _ = max(
313
+ max_font_sizes, key=lambda case: case[SUM_INDEX]
314
+ )
315
+ # self.logger.debug("used fs: g: {:.3f}, e: {:.3f}".format(german_max, english_max))
316
+ return german_max, english_max
317
+
318
+ def get_font_size(self, thing: BoxedThing):
319
+ """Determine font size of sign for thing.
320
+
321
+ Take font size in the thing.
322
+ Guess font size if not specified.
323
+
324
+ Returns:
325
+ (german font size, english font size)
326
+ """
327
+ default_german_font_size, default_english_font_size = self.guess_font_size(thing)
328
+ german_font_size = thing.sign.get(('fontsize', 0), default_german_font_size)
329
+ english_font_size = thing.sign.get(('fontsize', 1), default_english_font_size)
330
+ logging.info(
331
+ f"{thing.best('name')} font factor: (de) {german_font_size} (en) {english_font_size}"
332
+ )
333
+ return german_font_size, english_font_size
334
+
335
+ def create_latex(self, things: Iterable[BoxedThing]):
336
+ """Create latex code (as str) that shows all signs.
337
+
338
+ Arguments:
339
+ things: list of things to be described
340
+ """
341
+ content_latex = [self.pre]
342
+ current_line_filled_width = 0
343
+ for thing in [
344
+ tmp_thing for tmp_thing in things if tmp_thing.sign.should_be_printed()
345
+ ]:
346
+ if current_line_filled_width + self.width(thing) > PAPER_TEXT_WIDTH:
347
+ content_latex.append(
348
+ ""
349
+ ) # empty line in code makes new line (= row) in pdf
350
+ current_line_filled_width = 0
351
+ content_latex.append(self.create_sign(thing))
352
+ current_line_filled_width += self.width(thing)
353
+ content_latex.append(self.post)
354
+ return "\n".join(content_latex)
355
+
356
+ def get_values_for_template(self, thing: BoxedThing):
357
+ """Get values for the insertion into the templates.
358
+
359
+ Only the values that are common for portrait
360
+ and landscape template:
361
+ widthAdjusted (width - 0.14cm)
362
+ textscaleGerman
363
+ GermanName
364
+ textscaleEnglish
365
+ EnglishName
366
+ imagepath
367
+ imageheight
368
+ location
369
+ """
370
+ german = thing.sign.get(('name', 0), '')
371
+ english = thing.sign.get(('name', 1), '')
372
+
373
+ width = self.width(thing)
374
+ height = self.height(thing)
375
+ width = width - 0.14 # todo: fix latex template or create constant
376
+ insertions = {
377
+ "PrimaryName": sanitize_latex_code(german),
378
+ "SecondaryName": sanitize_latex_code(english),
379
+ "location": sanitize_latex_code(str(thing.where)),
380
+ "height": height,
381
+ "widthAdjusted": width,
382
+ }
383
+
384
+ if not insertions["location"]: # empty string
385
+ logging.warning(
386
+ f"Print sign for {german} ({english}) without location."
387
+ )
388
+
389
+ # todo: make image height configurable
390
+ if height < MINIMAL_HEIGHT_FOR_IMAGE:
391
+ insertions["imageheight"] = 0
392
+ insertions["vspace"] = r"-1.5\baselineskip"
393
+ else:
394
+ insertions["imageheight"] = height * IMAGE_SHARE
395
+ insertions["vspace"] = "1pt"
396
+
397
+ # at first only portrait sign, landscape sign can be implemented later
398
+ # if self.if_use_landscape_template(thing): # would need different condition
399
+ if image_path := thing.thing.image_path(): # not None or ""
400
+ rel_path_to_image = os.path.relpath(
401
+ image_path,
402
+ self.output_dir
403
+ )
404
+ insertions["imagepath"] = rel_path_to_image
405
+ else:
406
+ if insertions["imageheight"] > 0:
407
+ # otherwise no image is printed and we do not need an unneccessary warning
408
+ logging.getLogger(__name__).warning(
409
+ f"Missing image for {thing.best('name')}."
410
+ )
411
+ insertions["imagepath"] = DUMMY_IMAGE
412
+ try:
413
+ image_file = open(os.path.join(self.output_dir, DUMMY_IMAGE), mode="bw")
414
+ except FileExistsError:
415
+ pass
416
+ else:
417
+ dummy_image = importlib.resources.read_binary(TEMPLATE_PACKAGE, DUMMY_IMAGE)
418
+ with image_file:
419
+ image_file.write(dummy_image)
420
+ font_sizes = self.get_font_size(thing)
421
+ insertions["textscalePrimary"] = font_sizes[0] # /12
422
+ insertions["textscaleSecondary"] = font_sizes[1] # /12
423
+
424
+ insertions["locationShiftDown"] = thing.sign.get('location_shift_down',
425
+ STANDARD_LOCATION_SHIFT_DOWN)
426
+ return insertions
427
+
428
+ def create_sign(self, thing):
429
+ """Create a sign based on the template sign.tex."""
430
+ # text that is to be inserted into the template
431
+ insertions = self.get_values_for_template(thing)
432
+ # no landscape yet
433
+ # if self.if_use_landscape_template(thing):
434
+ # return self.signhtml_landscape.format(
435
+ # **insertions # unpacking dictionary
436
+ # )
437
+ return self.sign.format(**insertions)
438
+
439
+ def save_signs_latex(self, things: list[BoxedThing]) -> str:
440
+ """Save signs as tex-file to file path.
441
+
442
+ Ignore things that should not be printed as saved in things.sign['printed'].
443
+
444
+ Arguments:
445
+ things: list of things to visualize
446
+ Returns:
447
+ path to created file
448
+ """
449
+ things.sort(key=lambda t: self.height(t))
450
+ file_signs = os.path.join(self.output_dir, self.signs_file)
451
+ print(f"Print LaTeX file to {file_signs}.")
452
+ with open(file_signs, mode="w", encoding="UTF-8") as latex_file:
453
+ latex_file.write(self.create_latex(things))
454
+ return file_signs
455
+
456
+ def create_signs_pdf(self, things: list[BoxedThing]):
457
+ """Create a pdf and latex files with signs."""
458
+ self.save_signs_latex(things)
459
+ latex = subprocess.run(['latexmk', self.signs_file,
460
+ f'-aux-directory={AUX_DIR}',
461
+ "-pdf"],
462
+ cwd=self.output_dir,
463
+ capture_output=True,
464
+ text=True) # text=True makes output a str instead of bytes
465
+ with open(os.path.join(self.output_dir, AUX_DIR, 'latex_output.txt'), mode="w") as stdout_file:
466
+ stdout_file.write(latex.stdout)
467
+ with open(os.path.join(self.output_dir, AUX_DIR, 'latex_error.txt'), mode="w") as stderr_file:
468
+ stderr_file.write(latex.stderr)
469
+ if latex.returncode != 0:
470
+ print(f"Latex finished with returncode {latex.returncode}."
471
+ f"Check in {os.path.join(self.output_dir, AUX_DIR)} for details.")
flinventory/thing.py ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python
2
+ """A thing in an inventory."""
3
+ import logging
4
+ import os.path
5
+ import pathlib
6
+ from typing import override, Optional
7
+ import yaml
8
+
9
+ from . import constant
10
+ from . import defaulted_data
11
+
12
+
13
+ class Thing(defaulted_data.DefaultedDict):
14
+ """A thing in an inventory.
15
+
16
+ In addition to the functionality of DefaultedDict,
17
+ a thing has specific list and translated keys
18
+ and save and open from file functionality.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ data: defaulted_data.Data,
24
+ default: defaulted_data.Data,
25
+ options: constant.Options,
26
+ directory: str,
27
+ ):
28
+ """Create a new thing from some data.
29
+
30
+ Args:
31
+ data: data specific to this thing
32
+ default: default values as in DefaultedDict
33
+ options: inventory-wide options,
34
+ directory: where to save data of this thing
35
+ """
36
+ self._directory = None # None signals: no save
37
+ super().__init__(
38
+ data=data,
39
+ default=default,
40
+ non_defaulted=tuple(),
41
+ translated=("name", "name_alt", "description"),
42
+ lists=("name_alt", "part_of"),
43
+ default_order=("name", "name_alt", "part_of", "category", "url"),
44
+ options=options,
45
+ )
46
+ self._directory = directory
47
+ self._logger = logging.getLogger(__name__)
48
+
49
+ @property
50
+ def directory(self):
51
+ """The directory in which the thing data file is saved."""
52
+ return self._directory
53
+
54
+ @directory.setter
55
+ def directory(self, new_directory: str):
56
+ """Set the directory in which the data file is saved.
57
+
58
+ Save the data there.
59
+ The old data file is removed.
60
+ """
61
+ if self._directory is not None:
62
+ pathlib.Path(os.path.join(self._directory, constant.THING_FILE)).unlink(
63
+ missing_ok=True
64
+ )
65
+ assert new_directory is not None
66
+ self._directory = new_directory
67
+ self.save()
68
+
69
+ @override
70
+ def __setitem__(self, key, value):
71
+ """self[key] = value as in DefaultedDict but saved afterward."""
72
+ super().__setitem__(key, value)
73
+ if self._directory is not None:
74
+ self.save()
75
+
76
+ @override
77
+ def __delitem__(self, key):
78
+ """del self[key] as in DefaultedDict but saved afterward."""
79
+ super().__delitem__(key)
80
+ if self._directory is not None:
81
+ self.save()
82
+
83
+ def image_path(self) -> Optional[str]:
84
+ """Path to the image of the thing, if it exists.
85
+
86
+ If path is given and relative, it is assumed
87
+ that it is relative to my directory.
88
+ If it has no extension, try finding it with extension.
89
+ Returns:
90
+ If image is explicitly given, use this.
91
+ Otherwise, look in my directory for a suitably
92
+ named file with one of the image extensions.
93
+ Is relative path if my directory is relative.
94
+ None if no image was found.
95
+ """
96
+ path_base = self.get('image', constant.IMAGE_FILE_BASE)
97
+ if not os.path.isabs(path_base):
98
+ path_base = os.path.join(self.directory, path_base)
99
+ for extension in ('', *(f".{ext}" for ext in constant.IMAGE_FILE_TYPES)):
100
+ if os.path.isfile(path := path_base + extension):
101
+ return path
102
+ return None
103
+
104
+ @classmethod
105
+ def from_yaml_file(
106
+ cls, directory: str, default: defaulted_data.Data, options: constant.Options
107
+ ):
108
+ """Create a thing from data in a file.
109
+
110
+ Args:
111
+ directory: directory with sign file
112
+ default: thing for which this is a sign. Used as default data.
113
+ options: inventory-wide options
114
+ """
115
+ path = pathlib.Path(os.path.join(directory, constant.THING_FILE))
116
+ try:
117
+ thing_file = open(path, mode="r", encoding="utf-8")
118
+ except FileNotFoundError:
119
+ logging.getLogger(__name__).warning(f"{path} not found. That is unusual.")
120
+ return cls({}, default, options, directory)
121
+ with thing_file:
122
+ return cls(yaml.safe_load(thing_file), default, options, directory)
123
+
124
+ def save(self):
125
+ """Save data to yaml file.
126
+
127
+ Do not check if something is overwritten.
128
+
129
+ Delete file if no data is there to be written.
130
+ Todo: include git add and commit.
131
+ Raises:
132
+ TypeError: if directory is None
133
+ """
134
+ jsonable_data = self.to_jsonable_data()
135
+ path = pathlib.Path(os.path.join(self._directory, constant.THING_FILE))
136
+ if not jsonable_data:
137
+ if path.is_file():
138
+ self._logger.info(f"Delete {path}")
139
+ path.unlink(missing_ok=True)
140
+ else:
141
+ with open(path, mode="w", encoding="utf-8") as thing_file:
142
+ yaml.dump(jsonable_data, thing_file, **constant.YAML_DUMP_OPTIONS)
143
+ self._logger.info(
144
+ f"Saved {self.get(('name', 0), self._directory)} sign to {path}."
145
+ )
@@ -0,0 +1,6 @@
1
+ *.aux
2
+ *.fdb_latexmk
3
+ *.fls
4
+ *.log
5
+ *.pdf
6
+ *.synctex.gz
@@ -0,0 +1,26 @@
1
+ \fbox{{%
2
+ % need sizes:
3
+ % width (reduce by 0.24 cm)
4
+ % height
5
+ % text primary language
6
+ % text secondary language
7
+ % image path
8
+ % location shortcut
9
+ % optional: text size (default: \relscale{{1}} or something guessed based on existing algorithm)
10
+ % optional: image height (otherwise half of entire size)
11
+ \begin{{minipage}}[t][{height}cm][t]{{{widthAdjusted}cm}}
12
+ \vspace{{1mm}}
13
+ \setlength{{\parskip}}{{0pt}}
14
+ \centering
15
+ {{\relscale{{{textscalePrimary}}}
16
+ {PrimaryName}
17
+ }}
18
+
19
+ {{\relscale{{{textscaleSecondary}}} {SecondaryName}}}
20
+
21
+ \vspace*{{{vspace}}}
22
+ \includegraphics[height={imageheight}cm,keepaspectratio,width={widthAdjusted}cm]{{{imagepath}}}
23
+ \vspace*{{{locationShiftDown}}}
24
+ \flushright {{\tiny {location}}}
25
+ \end{{minipage}}
26
+ }}
@@ -0,0 +1 @@
1
+ \end{document}
@@ -0,0 +1,12 @@
1
+ \documentclass[12pt,a4paper]{article}
2
+ \usepackage[margin=0.5cm]{geometry}
3
+ \usepackage{xcolor}
4
+ \usepackage{graphicx}
5
+ \usepackage{adjustbox}
6
+ \usepackage[utf8]{inputenc}
7
+ \usepackage{relsize}
8
+ \usepackage{setspace}
9
+ \linespread{0.75}
10
+ \setlength\parindent{0pt}
11
+ \begin{document}
12
+ \setlength{\fboxsep}{0pt}