donelogger 0.1.7__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 rkskmt
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,415 @@
1
+ Metadata-Version: 2.4
2
+ Name: donelogger
3
+ Version: 0.1.7
4
+ Summary: Time long-running steps with two ordinary log lines: write [Start] and [Done], get the elapsed time filled in for you.
5
+ Author: rkskmt
6
+ License: MIT License
7
+
8
+ Copyright (c) 2022-2026 rkskmt
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/rkskmt/donelogger
29
+ Project-URL: Repository, https://github.com/rkskmt/donelogger
30
+ Project-URL: Issues, https://github.com/rkskmt/donelogger/issues
31
+ Keywords: logging,timer,elapsed,stopwatch,profiling
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3 :: Only
38
+ Classifier: Topic :: System :: Logging
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Requires-Python: >=3.7
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Dynamic: license-file
44
+
45
+ # donelogger
46
+
47
+ **Time long-running steps with two ordinary log lines.**
48
+
49
+ ![donelogger in action](assets/demo.gif)
50
+
51
+ Write `[Start]` when work begins and `[Done]` when it ends. donelogger fills
52
+ the elapsed time into the log line for you.
53
+
54
+ ```python
55
+ from donelogger import getLogger
56
+
57
+ logger = getLogger()
58
+
59
+ logger.info("[Start] Training model...")
60
+ train()
61
+ logger.info("[Done] Finished")
62
+ # -> +[Go Job] Training model...
63
+ # -> -[Done Job(1m23.40s)] Finished ← elapsed time, measured for you
64
+ ```
65
+
66
+ That's the whole idea: no `time.perf_counter()` variables, no manual
67
+ subtraction, no `f"{...:.3f}s"` formatting scattered through your code.
68
+
69
+ If you've ever written this:
70
+
71
+ ```python
72
+ t0 = time.perf_counter()
73
+ logger.info("Loading dataset...")
74
+ load_dataset()
75
+ logger.info(f"Finished loading in {time.perf_counter() - t0:.3f}s")
76
+ ```
77
+
78
+ you can write this instead:
79
+
80
+ ```python
81
+ logger.info("[Start] Loading dataset...")
82
+ load_dataset()
83
+ logger.info("[Done] Finished loading")
84
+ # -> -[Done Job(2.413s)] Finished loading
85
+ ```
86
+
87
+ The timing is just part of the log. Your call sites stay plain
88
+ `logger.info(...)`, and everything that is not a marker remains a normal log
89
+ message.
90
+
91
+ No tag needed for the basic case. When you want to time nested or overlapping
92
+ steps, add an optional `:tag` (`[Start:train]` ... `[Done:train]`).
93
+
94
+ > Battle-tested: donelogger runs in production internal tooling, where knowing
95
+ > "how long did each stage take?" across a long pipeline matters every day.
96
+
97
+ ---
98
+
99
+ ## Why donelogger?
100
+
101
+ ### 1. Make timing cheap enough to use everywhere
102
+
103
+ When timing is annoying, you only add it after something gets slow. donelogger
104
+ makes it cheap enough to leave timing breadcrumbs throughout a script, CLI,
105
+ batch job, ML run, or data pipeline:
106
+
107
+ ```python
108
+ logger.info("[Start] Download files")
109
+ download_files()
110
+ logger.info("[Done] Downloaded")
111
+
112
+ logger.info("[Start] Parse records")
113
+ parse_records()
114
+ logger.info("[Done] Parsed")
115
+
116
+ logger.info("[Start] Write output")
117
+ write_output()
118
+ logger.info("[Done] Wrote output")
119
+ ```
120
+
121
+ You get readable progress logs while the job runs, and elapsed times once each
122
+ step finishes.
123
+
124
+ ### 2. Stop subtracting timestamps by hand
125
+
126
+ The usual timing pattern is repetitive and easy to get slightly wrong:
127
+
128
+ ```python
129
+ t0 = time.perf_counter()
130
+ logger.info("Loading dataset...")
131
+ load_dataset()
132
+ logger.info(f"Finished loading in {time.perf_counter() - t0:.3f}s")
133
+ ```
134
+
135
+ donelogger keeps the stopwatch attached to the log line instead of your local
136
+ variables:
137
+
138
+ ```python
139
+ logger.info("[Start] Loading dataset...")
140
+ load_dataset()
141
+ logger.info("[Done] Finished loading")
142
+ ```
143
+
144
+ This really pays off once timings are **nested**. You often want an inner step's
145
+ time *and* the whole job's time. By the time the job ends, the start line has
146
+ scrolled far up the log; squinting at two timestamps to subtract them is exactly
147
+ the chore donelogger removes. (See [nested timing](#named-tags-time-nested-or-overlapping-work).)
148
+
149
+ ### 3. Skip the `logging` setup boilerplate
150
+
151
+ Getting plain `logging` to print the way you want takes a handler, a formatter,
152
+ a level, and a few lines of wiring before a single line shows up:
153
+
154
+ ```python
155
+ # Before — standard logging needs setup before it's usable
156
+ import logging, sys
157
+ logger = logging.getLogger("myapp")
158
+ logger.setLevel(logging.INFO)
159
+ handler = logging.StreamHandler(sys.stdout)
160
+ handler.setFormatter(logging.Formatter("%(asctime)s|%(levelname)s|%(message)s",
161
+ "%d/%m/%Y %H:%M:%S"))
162
+ logger.addHandler(handler)
163
+ ```
164
+
165
+ `getLogger()` does all of that for you — and hands back a **real
166
+ `logging.Logger`**, so levels, file output, and custom formats keep working
167
+ exactly as you'd expect:
168
+
169
+ ```python
170
+ # After — configured and ready in one line
171
+ from donelogger import getLogger
172
+ logger = getLogger() # console-ready, sensible defaults
173
+ logger = getLogger(logfile="app.log") # ...also writes to a rotating file
174
+ ```
175
+
176
+ Nothing proprietary to learn: it's `logging` underneath, just without the setup.
177
+
178
+ ## Features
179
+
180
+ - **Zero-boilerplate timing** — wrap work in `[Start]` / `[Done]` and get the elapsed time for free; no tag required.
181
+ - **One-line setup** — `getLogger()` returns a ready-to-use logger (handler, formatter, and level already wired) — no `logging` boilerplate.
182
+ - **Reads like normal logs** — markers are just text at the front of your message; nothing new to learn.
183
+ - **Named tags** — time overlapping or nested stages independently (`total`, `load`, `train`, …).
184
+ - **Cross-module** — start a timer in one file and finish it in another, as long as they share a logger name.
185
+ - **Human-friendly durations** — adaptive units from microseconds to hours (`300us`, `512.0ms`, `1.003s`, `1m15.40s`, `1h15m00s`), or force fixed seconds.
186
+ - **Drop-in `logging`** — `getLogger()` returns a real `logging.Logger`; all the usual `.info()` / `.warning()` / `.error()` work unchanged.
187
+ - **Optional rotating file log** — one argument enables a `RotatingFileHandler` with a detailed format.
188
+ - **Zero dependencies** — pure standard library, built on `logging.Formatter`.
189
+
190
+ ## Installation
191
+
192
+ ```bash
193
+ pip install git+https://github.com/rkskmt/donelogger.git
194
+ ```
195
+
196
+ Or install locally for development:
197
+
198
+ ```bash
199
+ git clone https://github.com/rkskmt/donelogger.git
200
+ cd donelogger
201
+ pip install -e .
202
+ ```
203
+
204
+ ## Quick Start
205
+
206
+ ```python
207
+ import time
208
+ from donelogger import getLogger
209
+
210
+ logger = getLogger()
211
+
212
+ # The basics: bare [Start] / [Done], no tag.
213
+ logger.info("[Start] Loading dataset...")
214
+ time.sleep(2)
215
+ logger.info("[Done] Finished loading")
216
+ # -> -[Done Job(2.001s)] Finished loading
217
+
218
+ # Need to time several things at once? Add an optional tag.
219
+ logger.info("[Start:train] Training model")
220
+ time.sleep(1)
221
+ logger.info("[Done:train]")
222
+ # -> -[Done train(1.002s)]
223
+ ```
224
+
225
+ ## Usage
226
+
227
+ ### Default timer (no tag required)
228
+
229
+ Tags are optional. Bare `[Start]` / `[Done]` use a default timer named `Job`:
230
+
231
+ ```python
232
+ logger.info("[Start] Processing")
233
+ # ... work ...
234
+ logger.info("[Done] Complete")
235
+ # -> -[Done Job(512.0ms)] Complete
236
+ ```
237
+
238
+ ### Named tags: time nested or overlapping work
239
+
240
+ Bare `[Start]` / `[Done]` track one thing at a time. Add a `:tag` to run several
241
+ timers at once — ideal when you want an inner step's time **and** the overall
242
+ time, without scrolling back up the log to subtract timestamps by hand:
243
+
244
+ ```python
245
+ logger.info("[Start:total] Pipeline starting")
246
+ logger.info("[Start:load] Loading dataset...")
247
+ load_dataset()
248
+ logger.info("[Done:load] Data ready") # inner step time
249
+ train()
250
+ logger.info("[Done:total] Pipeline finished") # whole-pipeline time
251
+ # -> -[Done load(2.001s)] Data ready
252
+ # -> -[Done total(1m25.40s)] Pipeline finished
253
+ ```
254
+
255
+ Timers are independent, so tags can nest (as above) or overlap freely without
256
+ clobbering each other:
257
+
258
+ ```python
259
+ logger.info("[Start:download] Downloading files")
260
+ logger.info("[Start:parse] Parsing config")
261
+ logger.info("[Done:parse] Config ready") # parse stops first
262
+ logger.info("[Done:download] Files saved") # download stops later
263
+ ```
264
+
265
+ ### Cross-module timing
266
+
267
+ `getLogger(name=...)` returns the **same logger instance** for a given name
268
+ (process-wide singleton), and the timer state lives on that logger. So any
269
+ module that calls `getLogger()` with the same name shares the same timers —
270
+ **start in one file, finish in another**:
271
+
272
+ ```python
273
+ # === data_loader.py ===
274
+ from donelogger import getLogger
275
+ getLogger().info("[Start:pipeline] Begin data pipeline")
276
+
277
+ # === trainer.py ===
278
+ from donelogger import getLogger
279
+ # ... after all stages finish ...
280
+ getLogger().info("[Done:pipeline] Pipeline complete")
281
+ # -> -[Done pipeline(42.300s)] Pipeline complete
282
+ ```
283
+
284
+ Loggers created with **different names keep independent timers**, so unrelated
285
+ components never clobber each other's tags.
286
+
287
+ ### Regular logging
288
+
289
+ Everything that isn't a marker passes straight through — donelogger is a normal
290
+ logger:
291
+
292
+ ```python
293
+ logger.info("Just a normal message")
294
+ logger.warning("This is a warning")
295
+ logger.error("Something went wrong")
296
+ ```
297
+
298
+ (Only `INFO`-level messages are scanned for markers; other levels are never
299
+ touched.)
300
+
301
+ ### Choosing the elapsed-time format
302
+
303
+ `elapsed_style` controls how durations are rendered (default `"adaptive"`):
304
+
305
+ ```python
306
+ logger = getLogger(elapsed_style="adaptive") # 300us, 512.0ms, 1.003s, 1m15.40s, 1h15m00s
307
+ logger = getLogger(elapsed_style="seconds") # always seconds: 0.300s, 0.512s, 1.003s, 75.400s
308
+ ```
309
+
310
+ ### File logging
311
+
312
+ Pass `logfile=` to also write to a rotating file (1 MB × 2 backups) with a
313
+ detailed, machine-friendly format:
314
+
315
+ ```python
316
+ logger = getLogger(name="myapp", logfile="app.log")
317
+ ```
318
+
319
+ ### Full configuration
320
+
321
+ ```python
322
+ import logging
323
+ from donelogger import getLogger
324
+
325
+ logger = getLogger(
326
+ name="myapp",
327
+ logLevel=logging.DEBUG,
328
+ logfile="app.log",
329
+ fmt="%(asctime)s|%(levelname)s|%(message)s",
330
+ datefmt="%d/%m/%Y %H:%M:%S",
331
+ elapsed_style="adaptive",
332
+ )
333
+ ```
334
+
335
+ ## Marker syntax
336
+
337
+ | Marker | Meaning |
338
+ |---|---|
339
+ | `[Start]` / `[Go]` | Start the default (`Job`) timer |
340
+ | `[Start:tag]` / `[Go:tag]` | Start a named timer |
341
+ | `[Done]` | Stop the default timer and print elapsed time |
342
+ | `[Done:tag]` | Stop a named timer and print elapsed time |
343
+
344
+ - The keyword is **case-insensitive** (`start`, `Start`, `go`, `Go`, `done`, `Done`).
345
+ - A marker is only recognized at the **very beginning** of the message.
346
+ - `[Done:tag]` without a prior `[Start:tag]` emits `*LOG ERROR* (tag is not started) {...}` so mistakes are obvious.
347
+ - A tag is not consumed on `[Done]`, so you can stop the same tag more than once (each reports elapsed since its `[Start]`).
348
+
349
+ ## Output format
350
+
351
+ ```
352
+ +[Go data_load] Loading dataset... ← '+' = timer started
353
+ -[Done data_load(2.001s)] Finished ← '-' = timer stopped, elapsed shown
354
+ ```
355
+
356
+ **`adaptive`** (default) picks a unit by magnitude:
357
+
358
+ | Duration | Rendered |
359
+ |---|---|
360
+ | 300 µs | `300us` |
361
+ | 5 ms | `5.0ms` |
362
+ | 0.512 s | `512.0ms` |
363
+ | 1.003 s | `1.003s` |
364
+ | 75.4 s | `1m15.40s` |
365
+ | 4500 s | `1h15m00s` |
366
+
367
+ **`seconds`** always uses seconds (handy when you post-process logs):
368
+
369
+ | Duration | Rendered |
370
+ |---|---|
371
+ | 0.005 s | `0.005s` |
372
+ | 75.4 s | `1m15.400s` |
373
+ | 4500 s | `75m00.000s` |
374
+
375
+ ## API
376
+
377
+ ### `getLogger(name="doneLogger", logLevel=logging.INFO, logfile=None, fmt=..., datefmt=..., elapsed_style="adaptive")`
378
+
379
+ Returns a configured `logging.Logger`. Calling it again with the same `name`
380
+ returns the cached instance (so configuration only happens once).
381
+
382
+ | Parameter | Default | Description |
383
+ |---|---|---|
384
+ | `name` | `"doneLogger"` | Logger name. Same name → same instance (and shared timers). `"root"` configures the root logger. |
385
+ | `logLevel` | `logging.INFO` | Level for the console handler / logger. |
386
+ | `logfile` | `None` | If set, also log to this file via a rotating handler (1 MB × 2 backups). |
387
+ | `fmt` | `%(asctime)s\|%(levelname)s\|%(message)s` | Console log format (standard `logging` format string). |
388
+ | `datefmt` | `%d/%m/%Y %H:%M:%S` | Timestamp format. |
389
+ | `elapsed_style` | `"adaptive"` | `"adaptive"` (µs→h) or `"seconds"` (always seconds). |
390
+
391
+ The package also exposes `DoneloggerFormatter`, `DoneloggerStreamHandler`, and
392
+ `LoggerManager` for advanced/custom wiring. `DoneloggerFormatter` is a drop-in
393
+ `logging.Formatter` (the standard `fmt` / `datefmt` / `style` arguments still
394
+ work), with one extra keyword-only argument, `elapsed_style`.
395
+
396
+ ## How it works
397
+
398
+ donelogger installs a custom `logging.Formatter` that inspects each `INFO`
399
+ message. A `[Start]`/`[Go]` marker records `time.perf_counter()` under the tag;
400
+ the matching `[Done]` looks it up, computes the delta, and rewrites the line
401
+ with the elapsed time. Because it's all in the formatter, your call sites stay
402
+ plain `logger.info(...)` calls and non-marker logging is unaffected.
403
+
404
+ ## Testing
405
+
406
+ ```bash
407
+ python -m unittest discover
408
+ ```
409
+
410
+ The suite is dependency-free and mocks `time.perf_counter`, so the timing
411
+ assertions are deterministic (no `sleep`, no flakiness).
412
+
413
+ ## License
414
+
415
+ [MIT](LICENSE)