arin-log 0.2.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.
arin_log/__init__.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ arin_log
3
+ --------
4
+ Een lichtgewicht, platformonafhankelijke logging-package voor
5
+ langlopende pipelines.
6
+
7
+ Twee rollen in één package:
8
+
9
+ 1. Een virtuele assistent (Arin) die op de console tegen de gebruiker
10
+ praat en hem op de hoogte houdt van wat het script doet.
11
+ 2. Een schoon, technisch logbestand dat alleen de bakens bevat:
12
+ start, exception, exit. Bedoeld voor de ontwikkelaar die later
13
+ moet reconstrueren wat er is gebeurd.
14
+
15
+ De gebruiker ziet een verhaal. De ontwikkelaar ziet een spoor.
16
+
17
+ Publieke API
18
+ ------------
19
+ Vanuit de logger:
20
+ log_start, log_notes, log_except, log_exit,
21
+ config_logs, file_exist, tijd
22
+
23
+ Vanuit de folder:
24
+ change_folder, get_folder
25
+
26
+ Vanuit de console:
27
+ cons_menu
28
+
29
+ Interne functies blijven bereikbaar via hun eigen module, bijvoorbeeld:
30
+ from arin_log.logger import not_set
31
+ from arin_log.folder import get_config
32
+ from arin_log.utils import load_json, save_json
33
+ from arin_log.console import cls, menu_dict
34
+ """
35
+
36
+ __version__ = "0.2.0"
37
+
38
+ from arin_log.logger import (
39
+ log_start,
40
+ log_notes,
41
+ log_except,
42
+ log_exit,
43
+ config_logs,
44
+ file_exist,
45
+ tijd,
46
+ )
47
+
48
+ from arin_log.folder import (
49
+ change_folder,
50
+ get_folder,
51
+ )
52
+
53
+ from arin_log.console import (
54
+ cons_menu,
55
+ )
56
+
57
+ __all__ = [
58
+ "log_start",
59
+ "log_notes",
60
+ "log_except",
61
+ "log_exit",
62
+ "config_logs",
63
+ "file_exist",
64
+ "tijd",
65
+ "change_folder",
66
+ "get_folder",
67
+ "cons_menu",
68
+ ]
arin_log/console.py ADDED
@@ -0,0 +1,91 @@
1
+ """
2
+ console.py
3
+ ----------
4
+ Console-interactie voor arin_log.
5
+
6
+ Bevat:
7
+ cls() - wist het scherm (Windows: cls, anders clear)
8
+ menu_dict(optlst) - bouwt een dict van genummerde menu-opties
9
+ cons_menu(optlst, ...) - toont een console-menu en retourneert
10
+ de gekozen functie als commando-string.
11
+
12
+ Dit is een aparte module omdat console-interactie (menu, scherm wissen)
13
+ niets met loggen te maken heeft. De logger logt; de console toont.
14
+ """
15
+
16
+ from os import system, name, path
17
+ from arin_log.logger import log_notes
18
+
19
+ br = chr(10)
20
+ prgnm = path.basename(__file__).split('.')[0]
21
+
22
+
23
+ def cls():
24
+ """Wist het scherm. Op Windows met 'cls', anders met 'clear'."""
25
+ system('cls' if name == 'nt' else 'clear')
26
+ return
27
+
28
+
29
+ def menu_dict(optlst):
30
+ """Bouwt een dict van genummerde menu-opties.
31
+
32
+ De opties krijgen de sleutels '1', '2', '3', ... in volgorde.
33
+ Daarnaast worden de aliassen 'e', 'exit', 'q' en 'quit' toegevoegd,
34
+ die allemaal naar 'exit' wijzen."""
35
+ opties = {}
36
+ idx = 1
37
+ for opt in optlst:
38
+ ids = str(idx)
39
+ opties[ids] = opt
40
+ idx += 1
41
+ opties['e'] = 'exit'
42
+ opties['exit'] = 'exit'
43
+ opties['q'] = 'exit'
44
+ opties['quit'] = 'exit'
45
+ return opties
46
+
47
+
48
+ def cons_menu(optlst, logloc=None):
49
+ """Toont een console-menu en retourneert de gekozen functie
50
+ als een commando-string (bijvoorbeeld 'mijn_functie()').
51
+
52
+ optlst - lijst met functienamen (strings, zonder haakjes).
53
+ 'exit' wordt automatisch toegevoegd als het ontbreekt.
54
+ logloc - context voor de logger; standaard None.
55
+
56
+ De functie blijft het menu tonen tot de gebruiker een geldige
57
+ keuze maakt. 'exit', 'e', 'q' en 'quit' geven 'exit(0)' terug."""
58
+
59
+ exts = ['e', 'exit', 'q', 'quit']
60
+ tlst = optlst
61
+ if 'exit' not in tlst:
62
+ tlst.append('exit')
63
+
64
+ opties = menu_dict(tlst)
65
+ keys = list(opties.keys())
66
+ lng = len(keys)
67
+ choice = '#'
68
+ com = ''
69
+ while choice not in keys:
70
+ cls()
71
+ log_notes([f'{logloc} ; console ; menu', br])
72
+ for key in keys:
73
+ tmp = opties[key].replace('_', ' ')
74
+ if key not in exts:
75
+ print(f'{key}. {tmp}')
76
+ print()
77
+ choice = input(f'Enter your choice ({keys[0]}-{keys[-5]}): ')
78
+ if choice.lower() in exts:
79
+ choice = choice.lower()
80
+ cls()
81
+ if choice in keys:
82
+ com = opties[choice]
83
+ tmp = opties[choice].replace('_', ' ')
84
+ x = keys.index(choice)
85
+ if x < lng - 5:
86
+ log_notes([br, f'Your ; choice ; {tmp}'], logloc=logloc)
87
+ if com.lower() == 'exit':
88
+ com = 'exit(0)'
89
+ else:
90
+ com = f'{com}()'
91
+ return com
arin_log/demo_log.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ demo_log.py
3
+ -----------
4
+ Eerste kennismaking met arin_log.
5
+
6
+ Laat zien:
7
+ - hoe je een hoofdmodule markeert met log_start / log_exit
8
+ - hoe je een exception logt met log_except
9
+ - hoe je console-output en logbestand-uitvoer combineert
10
+ via de logloc-parameter
11
+
12
+ Draai met:
13
+ python -m arin_log.demo_log
14
+ """
15
+
16
+ from arin_log.logger import log_start, log_notes, log_except, log_exit
17
+ from sys import _getframe
18
+ from os import path
19
+
20
+ prgnm = path.basename(__file__).split('.')[0]
21
+
22
+ def demo():
23
+ log_start(prgnm, logloc=prgnm)
24
+
25
+ log_notes("demo ; test ; logger")
26
+
27
+ try:
28
+ 1 / 0
29
+ except Exception:
30
+ log_except(_getframe(), "zero ; division ; exception", logloc=prgnm)
31
+
32
+ log_notes("continuing ; logging ; success")
33
+ log_notes("demo ; test ; success", logloc=prgnm)
34
+ log_exit(prgnm, logloc=prgnm)
35
+
36
+ if __name__ == "__main__":
37
+ demo()
arin_log/folder.py ADDED
@@ -0,0 +1,306 @@
1
+ """
2
+ folder.py
3
+ ---------
4
+ Beheert de config folder van arin_log.
5
+
6
+ Alle user-specifieke configuratie staat in één bestand:
7
+
8
+ %APPDATA%\\arin_log\\config_log.json
9
+
10
+ Dit bestand bevat:
11
+ - de kleuren (gekopieerd uit log_cnfg.json bij de eerste run)
12
+ - de gekozen log_folder
13
+
14
+ Bij de eerste run wordt het bestand aangemaakt:
15
+ 1. De gebruiker kiest een log folder via de dialoog.
16
+ 2. De kleuren worden uit log_cnfg.json (package folder) gelezen.
17
+ 3. Alles wordt opgeslagen in config_log.json.
18
+
19
+ Bij volgende runs wordt alleen config_log.json gelezen.
20
+ Als het bestand corrupt is, wordt het opnieuw aangemaakt.
21
+
22
+ Publieke functies:
23
+ get_folder() - geeft de huidige log_folder terug
24
+ change_folder(parent=None) - folder-dialoog, update config + log_folder
25
+ get_config() - geeft de volledige config dict terug
26
+
27
+ Interne functie:
28
+ _get_folder() - resolver, eenmalig bij import
29
+ """
30
+
31
+ from os import path, makedirs, environ
32
+ from arin_log.utils import load_json, save_json
33
+ br = chr(10)
34
+
35
+ CNFG_FOLDER = "arin_log"
36
+ CNFG_FILE = "config_log.json"
37
+ TEMPLATE_FILE = "log_cnfg.json"
38
+
39
+
40
+ # =====================================================================
41
+ # 1. INTERNE HELPERS — paden
42
+ # =====================================================================
43
+ def _config_folder():
44
+ """Bepaalt de config folder.
45
+
46
+ Windows: %APPDATA%\\arin_log
47
+ Anders: ~/.arin_log
48
+ Maakt de folder aan als die nog niet bestaat.
49
+ Retourneert het pad, of False bij falen."""
50
+ base = environ.get("APPDATA")
51
+ if base:
52
+ folder = path.join(base, CNFG_FOLDER)
53
+ else:
54
+ folder = path.join(path.expanduser("~"), ".arin_log")
55
+ try:
56
+ makedirs(folder, exist_ok=True)
57
+ except Exception:
58
+ return False
59
+ return folder
60
+
61
+
62
+ def _config_file():
63
+ """Pad naar config_log.json, of False."""
64
+ folder = _config_folder()
65
+ if not folder:
66
+ return False
67
+ return path.join(folder, CNFG_FILE)
68
+
69
+
70
+ def _template_file():
71
+ """Pad naar log_cnfg.json (in de package folder)."""
72
+ return path.join(path.dirname(__file__), TEMPLATE_FILE)
73
+
74
+
75
+ # =====================================================================
76
+ # 2. INTERNE HELPERS — config lezen en schrijven
77
+ # =====================================================================
78
+ def _read_config():
79
+ """Leest config_log.json en retourneert de dict.
80
+ Retourneert {} als het bestand ontbreekt."""
81
+ pf = _config_file()
82
+ if pf and path.isfile(pf):
83
+ d = load_json(pf)
84
+ if isinstance(d, dict):
85
+ return d
86
+ return {}
87
+
88
+
89
+ def _write_config(data):
90
+ """Schrijft data naar config_log.json.
91
+ Retourneert True bij succes, anders False."""
92
+ pf = _config_file()
93
+ if not pf:
94
+ return False
95
+ return save_json(pf, data, indent=4)
96
+
97
+
98
+ def _read_template():
99
+ """Leest log_cnfg.json uit de package folder.
100
+ Retourneert de dict, of {} als het bestand ontbreekt."""
101
+ pf = _template_file()
102
+ if path.isfile(pf):
103
+ d = load_json(pf)
104
+ if isinstance(d, dict):
105
+ return d
106
+ return {}
107
+
108
+
109
+ def _create_config(log_folder):
110
+ """Maakt config_log.json aan.
111
+
112
+ Kopieert alle sleutels uit log_cnfg.json (de template) en
113
+ voegt 'log_folder' toe. Retourneert True bij succes."""
114
+ d = _read_template()
115
+ d["log_folder"] = log_folder
116
+ return _write_config(d)
117
+
118
+
119
+ def _save_folder(folder):
120
+ """Update alleen de 'log_folder'-key in config_log.json.
121
+ Andere keys blijven staan. Retourneert True bij succes."""
122
+ d = _read_config()
123
+ if not d:
124
+ return _create_config(folder)
125
+ d["log_folder"] = folder
126
+ return _write_config(d)
127
+
128
+
129
+ def _save_failed():
130
+ """Meldt dat het opslaan is mislukt en stopt met exitcode 1."""
131
+ from sys import stderr, exit
132
+ stderr.write(f"folder ; selection ; not saved{br}administrator ; adjustments ; required")
133
+ exit(1)
134
+
135
+
136
+ # =====================================================================
137
+ # 3. INTERNE HELPERS — folder keuze
138
+ # =====================================================================
139
+ def _norm(p):
140
+ """Normaliseert een pad naar forward slashes.
141
+ Retourneert het pad als string, of de oorspronkelijke waarde
142
+ als die leeg is."""
143
+ return str(p).replace("\\", "/") if p else p
144
+
145
+
146
+ def manual_input():
147
+ """Vraagt de gebruiker om een folder via de console.
148
+
149
+ Blijft vragen tot de gebruiker een niet-lege invoer geeft en
150
+ de folder bestaat (of aangemaakt kan worden). Normaliseert het
151
+ pad naar forward slashes.
152
+
153
+ De hele lus zit in een buitenste try/except. Als er iets
154
+ onverwachts misgaat, geeft de functie False terug in plaats
155
+ van te crashen."""
156
+ try:
157
+ while True:
158
+ gekozen = input("Enter a folder path for your log files: ")
159
+ if not gekozen:
160
+ continue
161
+
162
+ gekozen = _norm(gekozen)
163
+
164
+ if not path.isdir(gekozen):
165
+ try:
166
+ makedirs(gekozen, exist_ok=True)
167
+ except Exception:
168
+ print(f"Could not create folder: {gekozen}")
169
+ continue
170
+
171
+ return gekozen
172
+ except Exception:
173
+ return False
174
+
175
+
176
+ def _folder_dialog(parent=None):
177
+ """Opent een folder-dialoog. tkinter wordt lokaal geïmporteerd.
178
+
179
+ parent=None -> tijdelijke verborgen root (batch-context).
180
+ parent=<Tk> -> gebruikt de bestaande root (GUI-context).
181
+ Retourneert het gekozen pad, of '' bij annuleren.
182
+
183
+ Als tkinter of de GUI niet beschikbaar is, valt de functie
184
+ terug op manual_input() (console-invoer). Als dat ook faalt,
185
+ stopt het programma met de melding 'platform ; graphic GUI ;
186
+ inexistent' en exitcode 1."""
187
+
188
+ try:
189
+ from tkinter import Tk, filedialog
190
+
191
+ root = None
192
+ folder = ""
193
+ eigen_root = False
194
+ if parent is None:
195
+ root = Tk()
196
+ root.withdraw()
197
+ eigen_root = True
198
+ parent = root
199
+ try:
200
+ folder = filedialog.askdirectory(
201
+ parent=parent,
202
+ title="Choose a folder for your log files"
203
+ )
204
+ finally:
205
+ if eigen_root:
206
+ try:
207
+ root.destroy()
208
+ except Exception:
209
+ pass
210
+
211
+ return folder if folder else ""
212
+
213
+ except Exception:
214
+ gekozen = manual_input()
215
+ if gekozen:
216
+ return gekozen
217
+ from sys import stderr, exit
218
+ stderr.write(f"platform ; graphic GUI ; inexistent{br}administrator ; adjustments ; required")
219
+ exit(1)
220
+
221
+
222
+ # =====================================================================
223
+ # 4. RESOLVER — eenmalig bij import
224
+ # =====================================================================
225
+ def _get_folder():
226
+ """Resolver: lees config_log.json, of maak het aan.
227
+
228
+ Retourneert altijd een geldig pad (string).
229
+ - Als config_log.json bestaat: log_folder daaruit.
230
+ - Anders: dialoog, dan config_log.json aanmaken.
231
+ - Bij corruptie: opnieuw aanmaken.
232
+
233
+ Wordt eenmalig aangeroepen bij het laden van de module.
234
+ Daarna is log_folder gezet en gebruikt de rest van de code
235
+ die waarde via get_folder() of folder.log_folder."""
236
+ d = _read_config()
237
+ folder = d.get("log_folder", False)
238
+
239
+ if folder:
240
+ return _norm(folder)
241
+
242
+ while True:
243
+ gekozen = _folder_dialog()
244
+ if gekozen:
245
+ gekozen = _norm(gekozen)
246
+ if not _create_config(gekozen):
247
+ _save_failed()
248
+ return gekozen
249
+
250
+
251
+ # =====================================================================
252
+ # 5. PUBLIEKE FUNCTIES
253
+ # =====================================================================
254
+ def get_folder():
255
+ """Geeft de huidige log_folder terug.
256
+
257
+ Altijd de actuele waarde. Aanroepen na change_folder() geeft
258
+ de nieuwe folder, niet de oude. Dit is de enige juiste manier
259
+ om de log folder op te vragen vanuit een andere module."""
260
+ return log_folder
261
+
262
+
263
+ def change_folder(parent=None):
264
+ """Opent de folder-dialoog, update config_log.json en de
265
+ globale log_folder en retourneert de nieuwe waarde.
266
+
267
+ parent=None -> tijdelijke verborgen root (batch-context).
268
+ parent=<Tk> -> gebruikt de bestaande root (GUI-context).
269
+
270
+ Bij annulering: retourneert False, log_folder blijft ongewijzigd.
271
+ Retourneert het nieuwe pad (string) bij succes.
272
+
273
+ Als de keuze niet opgeslagen kan worden, stopt het programma
274
+ met de melding 'folder ; selection ; not saved' en exitcode 1."""
275
+ global log_folder
276
+
277
+ gekozen = _folder_dialog(parent=parent)
278
+ if not gekozen:
279
+ return False
280
+
281
+ gekozen = _norm(gekozen)
282
+ if not _save_folder(gekozen):
283
+ _save_failed()
284
+ log_folder = gekozen
285
+ return log_folder
286
+
287
+
288
+ def get_config():
289
+ """Geeft de volledige config dict terug uit config_log.json.
290
+
291
+ Wordt gebruikt door logger.config_logs() om de kleuren te laden."""
292
+ return _read_config()
293
+
294
+
295
+ # =====================================================================
296
+ # 6. TOP-LEVEL UITVOERING — log_folder is klaar na import
297
+ # =====================================================================
298
+ log_folder = _get_folder()
299
+
300
+ # =====================================================================
301
+ # 7. TESTBLOK
302
+ # =====================================================================
303
+ if __name__ == "__main__":
304
+ print(f"log_folder: {log_folder}")
305
+ print(f"config: {get_config()}")
306
+ exit(0)
arin_log/log_cnfg.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "color_codes": {
3
+ "black": "30m",
4
+ "blue": "34m",
5
+ "cyan": "36m",
6
+ "darkgrey": "90m",
7
+ "green": "32m",
8
+ "lightblue": "94m",
9
+ "lightcyan": "96m",
10
+ "lightgreen": "92m",
11
+ "lightgrey": "37m",
12
+ "lightred": "91m",
13
+ "orange": "33m",
14
+ "pink": "95m",
15
+ "purple": "35m",
16
+ "red": "31m",
17
+ "yellow": "93m"
18
+ },
19
+ "color_words": {
20
+ "Arin": "cyan",
21
+ "completed": "cyan",
22
+ "modification": "cyan",
23
+ "pending": "cyan",
24
+ "terminated": "cyan",
25
+ "satisfied": "cyan",
26
+ "shutdown": "cyan",
27
+ "startup": "cyan",
28
+ "rebooted": "cyan",
29
+ "recovery": "cyan",
30
+ "running": "cyan",
31
+ "updated": "cyan",
32
+ "added": "green",
33
+ "succes": "green",
34
+ "success": "green",
35
+ "present": "green",
36
+ "restored": "green",
37
+ "saved": "green",
38
+ "corrupt": "lightred",
39
+ "error": "lightred",
40
+ "empty": "lightred",
41
+ "fail": "lightred",
42
+ "false": "lightred",
43
+ "inexistent": "lightred",
44
+ "invalid": "lightred",
45
+ "not exist": "lightred",
46
+ "not valid": "lightred",
47
+ "rude": "lightred",
48
+ "broken": "lightred",
49
+ "unrecoverable": "lightred",
50
+ "exception": "pink",
51
+ "changed": "orange",
52
+ "deleted": "orange",
53
+ "interrupted": "orange",
54
+ "interruption": "orange",
55
+ "fault": "orange",
56
+ "fault(s)": "orange",
57
+ "none": "orange",
58
+ "removed": "orange",
59
+ "timeout": "orange",
60
+ "unaltered": "orange",
61
+ "unfit": "orange"
62
+ }
63
+ }
arin_log/logger.py ADDED
@@ -0,0 +1,265 @@
1
+ """
2
+ logger.py
3
+ ---------
4
+ Centrale logging-module van arin_log.
5
+
6
+ Ondersteunende modules:
7
+ folder.py - beheert de config en de log folder.
8
+ utils.py - algemene helpers (load_json, save_json).
9
+ console.py - console-interactie (menu, scherm wissen).
10
+
11
+ Publieke functies:
12
+ log_start, log_notes, log_except, log_exit, config_logs,
13
+ file_exist, tijd, not_set, last_record.
14
+ """
15
+
16
+ from os import path, makedirs
17
+ from sys import _getframe, stderr, exit
18
+ from gc import collect
19
+ from re import sub
20
+ import time
21
+
22
+ from arin_log import folder
23
+
24
+ color_words = {}
25
+ color_codes = {}
26
+ prgnm = ''
27
+ tas = ''
28
+ tas2 = ''
29
+ br = chr(10)
30
+ recs_dic = {}
31
+
32
+
33
+ def not_set():
34
+ global prgnm
35
+ isns = True if prgnm == '' else False
36
+ return isns
37
+
38
+
39
+ def tijd():
40
+ return time.strftime("%H:%M:%S")
41
+
42
+
43
+ def last_record(logloc):
44
+ global recs_dic, br
45
+ result = recs_dic.get(logloc, br)
46
+ return result
47
+
48
+
49
+ def to_cons(note):
50
+ """Print note naar de console, met kleuren volgens color_words
51
+ en color_codes. Woorden worden alleen gekleurd als ze exact
52
+ overeenkomen (inclusief spaties)."""
53
+ global color_words, color_codes
54
+ clrlst = list(color_words.keys())
55
+ if any([x in note for x in clrlst]):
56
+ for w in clrlst:
57
+ if w in note:
58
+ tmp = sub(r'[^\w\s]', '', note)
59
+ tmp = sub(' +', ' ', tmp)
60
+ nwlst = tmp.split(' ')
61
+ twl = w.split(' ')
62
+ if all([x in nwlst for x in twl]):
63
+ clr = color_words.get(w, None)
64
+ clrc = color_codes.get(clr, None)
65
+ if clrc is not None:
66
+ note = note.replace(w, f"\033[{clrc}{w}\033[00m")
67
+ print(note)
68
+ return
69
+
70
+
71
+ def to_file(data, logloc=None):
72
+ global br, recs_dic
73
+ frm = _getframe()
74
+ logloc = 'error' if logloc is None else logloc
75
+ fnm = '.'.join([tas2, logloc, 'log'])
76
+ pf = path.join(folder.log_folder, fnm)
77
+ act = 'a' if path.isfile(pf) else 'w'
78
+ if isinstance(data, list):
79
+ records = data
80
+ else:
81
+ records = [str(data)]
82
+ ln = len(records)
83
+ if ln > 0:
84
+ try:
85
+ with open(pf, act, encoding="utf-8") as txtFile:
86
+ for rec in records:
87
+
88
+ if (last_record(logloc) == br) and (rec == br):
89
+ continue
90
+ else:
91
+ if rec == br:
92
+ recs_dic[logloc] = br
93
+ rec = ''
94
+ else:
95
+ recs_dic[logloc] = 'ne'
96
+ rec = rec + br
97
+ txtFile.write(rec)
98
+
99
+ if any([x in rec for x in ['completed', 'terminated']]):
100
+ lng = len(rec)
101
+ if 'completed' in rec:
102
+ lijn = '-' * (lng - 1) + br
103
+ elif 'terminated' in rec:
104
+ lijn = '_' * (lng - 1) + br
105
+ txtFile.write(lijn)
106
+ recs_dic[logloc] = 'ne'
107
+ txtFile.close()
108
+ except Exception:
109
+ st = ' ; '.join(['logging', fnm, 'fail'])
110
+ log_except(frm, st, mod=prgnm, logloc=logloc)
111
+ else:
112
+ st = ' ; '.join(['log', fnm, 'empty'])
113
+ log_except(frm, st, mod=prgnm, logloc=logloc)
114
+ return
115
+
116
+
117
+ def log_notes(notes, cons=None, logloc=None, end=False, ar_on=True):
118
+ """Schrijft notes naar de console (indien cons=True) en naar het
119
+ logbestand (indien logloc is meegegeven).
120
+
121
+ end=True -> gebruik 'Arin' als prefix (voor afsluitende berichten).
122
+ end=False -> gebruik 'Arin :' als prefix (of geen prefix als ar_on=False).
123
+ """
124
+ global br, prgnm
125
+ cons = True if cons is None else cons
126
+ st = False if logloc is None else True
127
+ if isinstance(notes, str):
128
+ notes = [notes]
129
+ for el in notes:
130
+ if cons:
131
+ if end:
132
+ if el == br:
133
+ print()
134
+ else:
135
+ to_cons(' '.join(['Arin', el]))
136
+ else:
137
+ if el == br:
138
+ print()
139
+ elif ar_on:
140
+ to_cons(' '.join(['Arin :', el]))
141
+ else:
142
+ to_cons(el)
143
+ if st:
144
+ to_file(el, logloc=logloc)
145
+ return
146
+
147
+
148
+ def log_start(pnm, imp=None, cons=None, logloc=None):
149
+ """Markeert het begin van een module-run en ververst de datum
150
+ waarop het logbestand is gebaseerd (tas2). Zo komt een run die
151
+ na middernacht start in het logbestand van de nieuwe dag."""
152
+ global br, tas2
153
+ imp = False if imp is None else imp
154
+ typ = 'pending' if imp else 'running'
155
+ cons = True if cons is None else cons
156
+ tas2 = time.strftime('%y-%m-%d')
157
+ lijn = ' ; '.join([pnm, tijd(), typ])
158
+ if last_record(logloc) != br:
159
+ lijn = [br, lijn]
160
+ if not imp:
161
+ lijn = [lijn, br]
162
+ log_notes(lijn, cons=cons, logloc=logloc)
163
+ return
164
+
165
+
166
+ def log_except(frm, note, mod=None, cons=None, logloc=None):
167
+ global br
168
+ logloc = 'error' if logloc is None else logloc
169
+ cons = True if cons is None else cons
170
+ if last_record(logloc) == br:
171
+ notes = []
172
+ else:
173
+ notes = [br]
174
+ flpf = frm.f_code.co_filename
175
+ mod = path.basename(flpf).split('.')[0] if mod is None else mod
176
+ fncpos = '.'.join([mod, frm.f_code.co_name, str(frm.f_lineno)])
177
+ lijn = ' ; '.join([fncpos, tijd(), 'exception'])
178
+ notes.append(lijn)
179
+ if isinstance(note, str):
180
+ notes.append(note)
181
+ elif isinstance(note, list):
182
+ notes = notes + note
183
+ log_notes(notes, logloc=logloc, cons=cons)
184
+ return
185
+
186
+
187
+ def log_exit(prginf, imp=None, cons=None, logloc=None):
188
+ global br
189
+ imp = False if imp is None else imp
190
+ cons = True if cons is None else cons
191
+ notes = [prginf] if isinstance(prginf, str) else prginf
192
+ if imp:
193
+ notes[-1] = ' ; '.join([notes[-1], tijd(), 'completed'])
194
+ else:
195
+ if last_record(logloc) != br:
196
+ notes = [br] + notes
197
+ notes[-1] = ' ; '.join([notes[-1], tijd(), 'terminated'])
198
+ notes.append(br)
199
+ log_notes(notes, logloc=logloc, cons=cons)
200
+ collect()
201
+ if imp:
202
+ return
203
+ else:
204
+ import sys
205
+ sys.tracebacklimit = 0
206
+ sys.exit(0)
207
+
208
+
209
+ def config_logs():
210
+ """Leest de kleuren uit config_log.json (via folder.get_config)
211
+ en zet ze als globals.
212
+
213
+ De sleutel 'log_folder' wordt overgeslagen; die hoort bij folder.py."""
214
+ jsnd = folder.get_config()
215
+ if len(jsnd) > 0:
216
+ for k in jsnd.keys():
217
+ if k == "log_folder":
218
+ continue
219
+ globals()[k] = jsnd.get(k, False)
220
+ return
221
+
222
+
223
+ def file_exist(pf, timeout=None):
224
+ from time import sleep
225
+ timeout = 0.24 if timeout is None else timeout
226
+ if pf is None:
227
+ return False
228
+ else:
229
+ st = True
230
+ slt = 0.08
231
+ tt = 0
232
+ while not path.isfile(pf):
233
+ tt = tt + slt
234
+ if tt >= timeout:
235
+ st = False
236
+ break
237
+ sleep(slt)
238
+ return st
239
+
240
+
241
+ def _ensure_log_folder():
242
+ """Controleert of folder.log_folder bestaat en maakt hem zo nodig
243
+ aan. Lukt dat niet, dan stopt het programma met een duidelijke
244
+ melding op stderr en exitcode 1."""
245
+ try:
246
+ makedirs(folder.log_folder, exist_ok=True)
247
+ except Exception:
248
+ stderr.write(f"log ; folder ; inexistent{br}administrator ; adjustments ; required")
249
+ exit(1)
250
+ return
251
+
252
+
253
+ if not_set():
254
+ prgnm = path.basename(__file__).split('.')[0]
255
+ tas2 = time.strftime('%y-%m-%d')
256
+ tas = time.strftime('%Y-%m-%d')
257
+ _ensure_log_folder()
258
+ config_logs()
259
+
260
+
261
+ if __name__ == "__main__":
262
+ from arin_log.demo_log import demo
263
+ demo()
264
+ exit(0)
265
+
arin_log/utils.py ADDED
@@ -0,0 +1,61 @@
1
+ from os import path
2
+ from json import dump, load
3
+
4
+ # Vaste ANSI-kleuren voor _warn.
5
+ # Los van de user-config (log_cnfg.json), omdat utils.py een blad is
6
+ # en geen toegang heeft tot de logger.
7
+ # Kleurcodes uit log_cnfg.json: cyan=36m, pink=95m, lightred=91m.
8
+ _CYAN = "\033[36m"
9
+ _RESET = "\033[00m"
10
+
11
+
12
+ def _warn(note, word, color):
13
+ """Print een waarschuwing op de console.
14
+
15
+ 'Arin' wordt altijd cyaan. Het opgegeven woord krijgt de
16
+ opgegeven kleurcode. Wordt alleen gebruikt bij interne fouten
17
+ in utils.py (corrupt of ontbrekend bestand).
18
+
19
+ Deze functie staat los van de logger: utils.py is een blad en
20
+ mag niet van logger afhangen (zou een circulaire import geven)."""
21
+ note = note.replace(word, f"\033[{color}{word}{_RESET}")
22
+ print(f"{_CYAN}Arin{_RESET} : {note}")
23
+ return
24
+
25
+
26
+ def load_json(pf):
27
+ """Leest een JSON-bestand en retourneert de inhoud als dict.
28
+ Retourneert {} als het bestand ontbreekt of corrupt is.
29
+
30
+ Bij ontbreken of corruptie wordt een melding op de console
31
+ geprint, met Arin-prefix en vaste kleuren."""
32
+ jsnd = {}
33
+ fnm = path.basename(pf)
34
+ if path.isfile(pf):
35
+ try:
36
+ with open(pf, encoding='utf-8') as fil:
37
+ jsnd = load(fil)
38
+ except Exception:
39
+ _warn(f"data ; {fnm} ; corrupt", "corrupt", "95m")
40
+ else:
41
+ _warn(f"file ; {fnm} ; inexistent", "inexistent", "91m")
42
+ return jsnd
43
+
44
+
45
+ def save_json(pf, data, indent=None):
46
+ """Schrijft data als JSON naar pf.
47
+
48
+ indent=None of een negatief getal -> geen indentering.
49
+ indent=<int> >= 0 -> indentering met die breedte.
50
+
51
+ Retourneert True bij succes, anders False.
52
+ Bij een fout wordt een melding op de console geprint."""
53
+ indent = indent if isinstance(indent, int) and indent >= 0 else None
54
+ fnm = path.basename(pf)
55
+ try:
56
+ with open(pf, 'w', encoding='utf-8') as fil:
57
+ dump(data, fil, ensure_ascii=False, indent=indent)
58
+ return True
59
+ except Exception:
60
+ _warn(f"save ; {fnm} ; fail", "fail", "91m")
61
+ return False
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: arin-log
3
+ Version: 0.2.0
4
+ Summary: A lightweight logging package for long-running pipelines: a virtual assistant for the user, a clean archive for the developer.
5
+ Author-email: Marcus Aurelius <marcus.aurelius.hu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/marcus-aurelius-hu/arin_log
8
+ Project-URL: Source, https://github.com/marcus-aurelius-hu/arin_log
9
+ Project-URL: Issues, https://github.com/marcus-aurelius-hu/arin_log/issues
10
+ Keywords: logging,pipeline,console,logger,arin
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Logging
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # arin-log
27
+
28
+ A lightweight, cross-platform logging package for long-running pipelines.
29
+ A virtual assistant for the user. A clean archive for the developer.
30
+
31
+ ---
32
+
33
+ ## What is `arin_log`?
34
+
35
+ `arin_log` combines two roles in one package:
36
+
37
+ 1. **A virtual assistant (Arin)** who talks to the user on the console
38
+ and keeps them informed about what the script is doing.
39
+ 2. **A clean, technical log file** that contains only the beacons:
40
+ start, exception, exit. Meant for the developer who has to
41
+ reconstruct later what happened.
42
+
43
+ The user sees a story. The developer sees a trail. That is the core.
44
+
45
+ **Arin is not an AI.** She is a voice — a smart logger with a
46
+ character. This is a deliberate choice not to abuse the AI hype.
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install arin-log
54
+ ```
55
+
56
+ On first import, a folder dialog appears asking where you want to
57
+ store your log files. The choice, together with the default colors,
58
+ is saved in:
59
+
60
+ - **Windows:** `%APPDATA%\arin_log\config_log.json`
61
+ - **Linux/macOS:** `~/.arin_log/config_log.json`
62
+
63
+ The dialog appears only once. On every next run, `arin_log` reads
64
+ this file and skips the dialog.
65
+
66
+ If you want to change the log folder later:
67
+
68
+ ```python
69
+ from arin_log import change_folder
70
+ change_folder()
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Quick example
76
+
77
+ ```python
78
+ from arin_log import log_start, log_notes, log_except, log_exit
79
+ from sys import _getframe
80
+
81
+ log_start("my_pipeline", logloc="pipeline")
82
+
83
+ try:
84
+ log_notes("startup ; loading data", logloc="pipeline")
85
+ # ... your work ...
86
+ log_notes("success ; data loaded", logloc="pipeline")
87
+ except Exception:
88
+ log_except(_getframe(), "data ; loading ; fail", logloc="pipeline")
89
+ log_exit("my_pipeline", imp=False, logloc="pipeline")
90
+
91
+ log_exit("my_pipeline", imp=True, logloc="pipeline")
92
+ ```
93
+
94
+ **What the user sees on the console:**
95
+
96
+ ```
97
+ Arin : startup ; loading data
98
+ Arin : success ; data loaded
99
+ Arin my_pipeline ; 14:32:11 ; completed
100
+ ```
101
+
102
+ **What ends up in the log file:**
103
+
104
+ ```
105
+ 26-10-04.pipeline.log
106
+
107
+ my_pipeline ; 14:32:11 ; running
108
+
109
+ startup ; loading data
110
+ success ; data loaded
111
+
112
+ my_pipeline ; 14:32:11 ; completed
113
+ ------------------------------------------------------------
114
+ ```
115
+
116
+ ---
117
+
118
+ ## What makes `arin_log` different?
119
+
120
+ | Aspect | `arin_log` | Loguru / stdlib / structlog |
121
+ |---|---|---|
122
+ | **Context propagation** | One parameter `logloc` travels through the whole call chain; every submodule becomes part of the same log file | `LoggerAdapter`, `contextvars`, or manual config |
123
+ | **Console vs. log file** | Deliberately separated: console = everything (with Arin as a voice), log file = only the beacons | Everything goes to the same handlers |
124
+ | **Archive** | One log file per main module per day, with automatic section separation per subtask | Scattered files or one big file |
125
+ | **Arin as virtual assistant** | A character who guides the user, not a log-level prefix | No equivalent |
126
+ | **Exception notation** | `module.function.linenumber ; time ; exception` — compact and directly usable | Full traceback or `exc_info=True` |
127
+
128
+ **Core USP:** `logloc` + daily grouping per main module + separation of
129
+ console and archive + Arin.
130
+
131
+ ---
132
+
133
+ ## How it works
134
+
135
+ The package consists of four modules, each with a clear task:
136
+
137
+ - **`logger.py`** — the heart: `log_start`, `log_notes`, `log_except`,
138
+ `log_exit`, and the helpers.
139
+ - **`folder.py`** — manages the config folder and the log folder.
140
+ - **`console.py`** — console interaction: clear screen, menus.
141
+ - **`utils.py`** — JSON helpers (`load_json`, `save_json`).
142
+
143
+ ### Configuration
144
+
145
+ - **`log_cnfg.json`** ships with the package as a template: the default
146
+ color words and color codes for the console.
147
+ - **`config_log.json`** lives in `%APPDATA%\arin_log\` (Windows) or
148
+ `~/.arin_log/` (Linux/macOS). On first run it is created by copying
149
+ the colors from `log_cnfg.json` and adding your chosen log folder.
150
+
151
+ To customize colors, edit `config_log.json` in the config folder
152
+ above. Your changes survive `pip install --upgrade arin-log`, because
153
+ the file is never touched by pip.
154
+
155
+ ---
156
+
157
+ ## The log folder
158
+
159
+ `arin_log` stores its files in a folder of your choice. On first use,
160
+ a folder dialog appears. The choice is saved in `config_log.json`
161
+ (see Configuration), so it survives across sessions and is never
162
+ overwritten by a `pip install --upgrade`.
163
+
164
+ If `config_log.json` is missing or corrupt, the dialog appears again —
165
+ the package has no fallback folder. The user is expected to choose.
166
+
167
+ ---
168
+
169
+ ## Requirements
170
+
171
+ - Python 3.9 or higher
172
+
173
+ No external dependencies.
174
+
175
+ ---
176
+
177
+ ## License
178
+
179
+ MIT — see [LICENSE](LICENSE).
180
+
181
+ ---
182
+
183
+ ## Status
184
+
185
+ **Alpha (0.2.0).** The core works. Tests are still in development.
@@ -0,0 +1,12 @@
1
+ arin_log/__init__.py,sha256=hP5IEKp9lGNVjzh02xRoFbr4aIv_RP0to764h8FMWDg,1437
2
+ arin_log/console.py,sha256=UlInyJkt0468K0X8r-GfPHu5CZM0TlVtDB1YrZfiKQ4,2868
3
+ arin_log/demo_log.py,sha256=WVwDj_G-c32OEZstqurxtbx0_f_zBU0ECvEfaHKBf3c,873
4
+ arin_log/folder.py,sha256=bttm6LWH7j04dYTA_2JBW6bS_6BIezLWRasW90mvMCw,9709
5
+ arin_log/log_cnfg.json,sha256=-N7A_663Xbc4NQGofNXc99DCpBwUz5wYVWoIzI3SgpQ,1526
6
+ arin_log/logger.py,sha256=3Thbt_iZ54UBsI59lucqOJ2pT3hcfMz6HMEyRsQ40-w,8008
7
+ arin_log/utils.py,sha256=04oJk2328-wxnsDwXWPojcfzu946EpSbRBeoeI2QUx0,2125
8
+ arin_log-0.2.0.dist-info/licenses/LICENSE,sha256=HXmJEUrdgvHJ6S_VAXL9ZWRgKS0KyqzEu0Q_TWxx0nQ,1071
9
+ arin_log-0.2.0.dist-info/METADATA,sha256=RqH2QHQwlzeAUSdz6BBXcWYAHmJYuRnn1zuUU2MWyZU,5936
10
+ arin_log-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ arin_log-0.2.0.dist-info/top_level.txt,sha256=Z70CumXkXyWyXM6YZk86a1GVb8q6hdsLC7lNpbEJQpY,9
12
+ arin_log-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcus Aurelius
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ arin_log