tailr 1.0.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.
tailr/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """tailr - colorized live log viewer."""
2
+
3
+ __version__ = "1.0.0"
tailr/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from tailr.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
tailr/cli.py ADDED
@@ -0,0 +1,48 @@
1
+ """tailr command-line interface."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from tailr import __version__
7
+ from tailr.core import follow_stdin, tail_file
8
+
9
+
10
+ def build_parser():
11
+ p = argparse.ArgumentParser(
12
+ prog="tailr",
13
+ description="Colorized live log viewer. Tail files or piped stdin with "
14
+ "regex level and keyword highlighting. Zero dependencies.",
15
+ )
16
+ p.add_argument("path", nargs="*", default=["-"],
17
+ help="file to tail, or '-' for stdin (default)")
18
+ p.add_argument("-f", "--follow", action="store_true",
19
+ help="keep following appended lines (like tail -f)")
20
+ p.add_argument("-n", "--lines", type=int, default=10,
21
+ help="number of lines to show from the end (default 10)")
22
+ p.add_argument("-g", "--grep", help="only show lines matching this regex")
23
+ p.add_argument("-l", "--level", help="only show lines with this log level "
24
+ "(e.g. ERROR, INFO, WARN)")
25
+ p.add_argument("--version", action="version", version=f"tailr {__version__}")
26
+ return p
27
+
28
+
29
+ def main(argv=None):
30
+ args = build_parser().parse_args(argv)
31
+ paths = args.path
32
+ if any(p == "-" for p in paths) and len(paths) > 1:
33
+ print("tailr: mixing stdin ('-') with files is not supported", file=sys.stderr)
34
+ return 1
35
+ if paths == ["-"]:
36
+ lines = follow_stdin(grep=args.grep, level=args.level)
37
+ else:
38
+ for path in paths:
39
+ try:
40
+ tail_file(path, follow=args.follow, lines=args.lines,
41
+ grep=args.grep, level=args.level)
42
+ except FileNotFoundError:
43
+ print(f"tailr: no such file: {path}", file=sys.stderr)
44
+ return 1
45
+ except IsADirectoryError:
46
+ print(f"tailr: is a directory: {path}", file=sys.stderr)
47
+ return 1
48
+ return 0
tailr/core.py ADDED
@@ -0,0 +1,173 @@
1
+ """tailr - colorized live log viewer.
2
+
3
+ Tail files (or stdin) with regex-driven level and keyword highlighting.
4
+ Zero dependencies.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import sys
10
+ import time
11
+
12
+ LEVELS = {
13
+ "TRACE": "\033[38;5;245m",
14
+ "DEBUG": "\033[38;5;244m",
15
+ "INFO": "\033[38;5;39m",
16
+ "WARN": "\033[38;5;214m",
17
+ "WARNING": "\033[38;5;214m",
18
+ "ERROR": "\033[38;5;196m",
19
+ "FATAL": "\033[38;5;201m",
20
+ "CRITICAL": "\033[38;5;201m",
21
+ }
22
+ RESET = "\033[0m"
23
+
24
+ _TIMESTAMP = re.compile(
25
+ r"(?P<ts>\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?(?:Z|[+-]\d{2}:?\d{2})?)"
26
+ )
27
+ _ISO_TIME = re.compile(r"\d{2}:\d{2}:\d{2}(?:[.,]\d+)?")
28
+ _KEYWORDS_TO_HIGHLIGHT = ("error", "fail", "exception", "traceback", "panic", "fatal")
29
+
30
+ _LEVEL_MATCH = re.compile(
31
+ r"\b(" + "|".join(sorted(LEVELS.keys(), key=len, reverse=True)) + r")\b"
32
+ )
33
+
34
+ NO_COLOR = os.environ.get("NO_COLOR") is not None
35
+ FORCE_COLOR = os.environ.get("FORCE_COLOR") not in (None, "", "0")
36
+
37
+
38
+ def use_color():
39
+ if NO_COLOR:
40
+ return False
41
+ if FORCE_COLOR:
42
+ return True
43
+ return sys.stdout.isatty()
44
+
45
+
46
+ def _level_color(token):
47
+ key = token.upper()
48
+ return LEVELS.get(key, "\033[38;5;248m")
49
+
50
+
51
+ def colorize(line):
52
+ """Apply ANSI color to a single log line (best-effort, safe)."""
53
+ if not line:
54
+ return line
55
+ work = line
56
+ m = _TIMESTAMP.search(work)
57
+ color = "\033[38;5;240m"
58
+ if m:
59
+ work = work[: m.start()] + color + work[m.start() : m.end()] + RESET + work[m.end():]
60
+ else:
61
+ time_m = _ISO_TIME.search(work)
62
+ if time_m:
63
+ work = work[: time_m.start()] + color + work[time_m.start() : time_m.end()] + RESET + work[time_m.end():]
64
+ work = _highlight_level_and_keywords(work)
65
+ if any(k in line.lower() for k in _KEYWORDS_TO_HIGHLIGHT):
66
+ work = _highlight_keywords(work)
67
+ return work
68
+
69
+
70
+ def _highlight_level_and_keywords(line):
71
+ out = []
72
+ pos = 0
73
+ for lm in _LEVEL_MATCH.finditer(line):
74
+ out.append(line[pos : lm.start()])
75
+ out.append(_level_color(lm.group(1)) + lm.group(1) + RESET)
76
+ pos = lm.end()
77
+ out.append(line[pos:])
78
+ return "".join(out)
79
+
80
+
81
+ def _highlight_keywords(line):
82
+ def sub(m):
83
+ return "\033[1m\033[38;5;203m" + m.group(0) + RESET
84
+
85
+ return re.sub(r"\b(error|fail(?:ure|ed)?|exception|traceback|panic)\b",
86
+ sub, line, flags=re.IGNORECASE)
87
+
88
+
89
+ def tail_file(path, follow=False, lines=10, grep=None, level=None, stream=None):
90
+ """Tail a file, emitting colorized lines to ``stream`` (default stdout).
91
+
92
+ Returns the list of emitted raw lines (uncolored) for testing.
93
+ """
94
+ if stream is None:
95
+ stream = sys.stdout
96
+ use_colors = use_color()
97
+ grep_re = re.compile(grep, re.IGNORECASE) if grep else None
98
+ level_re = re.compile(r"\b" + re.escape(level) + r"\b", re.IGNORECASE) if level else None
99
+
100
+ with open(path, "r", errors="replace") as fh:
101
+ if not follow:
102
+ _seek_tail(fh, lines)
103
+ emitted = []
104
+ while True:
105
+ read_lines = fh.readlines()
106
+ for raw_line in read_lines:
107
+ line = raw_line.rstrip("\n")
108
+ if level_re and not level_re.search(line):
109
+ continue
110
+ if grep_re and not grep_re.search(line):
111
+ continue
112
+ out = colorize(line) if use_colors else line
113
+ stream.write(out + "\n")
114
+ emitted.append(line)
115
+ stream.flush()
116
+ if not follow:
117
+ return emitted
118
+ time.sleep(0.1)
119
+ if not fh.fp:
120
+ return emitted
121
+
122
+
123
+ def _seek_tail(fh, lines):
124
+ """Position ``fh`` so that readlines() returns exactly the last ``lines`` lines."""
125
+ if lines <= 0:
126
+ fh.seek(0, 2)
127
+ return
128
+ fh.seek(0, 2)
129
+ size = fh.tell()
130
+ if size == 0:
131
+ fh.seek(0)
132
+ return
133
+ fh.seek(size - 1)
134
+ last = fh.read(1)
135
+ fh.seek(0)
136
+ newlines = 0
137
+ while True:
138
+ data = fh.read(64 * 1024)
139
+ if not data:
140
+ break
141
+ newlines += data.count("\n")
142
+ total_lines = newlines + (1 if last != "\n" else 0)
143
+ to_drop = total_lines - lines
144
+ if to_drop <= 0:
145
+ fh.seek(0)
146
+ return
147
+ fh.seek(0)
148
+ for _ in range(to_drop):
149
+ if not fh.readline():
150
+ break
151
+
152
+
153
+ def follow_stdin(grep=None, level=None, stream=None):
154
+ """Tail stdin (for piped input), emitting colorized lines."""
155
+ if stream is None:
156
+ stream = sys.stdout
157
+ use_colors = use_color()
158
+ grep_re = re.compile(grep, re.IGNORECASE) if grep else None
159
+ level_re = re.compile(r"\b" + re.escape(level) + r"\b", re.IGNORECASE) if level else None
160
+ lines = []
161
+ try:
162
+ for raw_line in sys.stdin:
163
+ line = raw_line.rstrip("\n")
164
+ if level_re and not level_re.search(line):
165
+ continue
166
+ if grep_re and not grep_re.search(line):
167
+ continue
168
+ out = colorize(line) if use_colors else line
169
+ stream.write(out + "\n")
170
+ lines.append(line)
171
+ except KeyboardInterrupt:
172
+ pass
173
+ return lines
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: tailr
3
+ Version: 1.0.0
4
+ Summary: Colorized live log viewer for the terminal. Zero dependencies.
5
+ Author: dsk-dev-ai
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/dsk-dev-ai/tailr
8
+ Project-URL: Source, https://github.com/dsk-dev-ai/tailr
9
+ Project-URL: Issues, https://github.com/dsk-dev-ai/tailr/issues
10
+ Project-URL: Funding, https://github.com/sponsors/dsk-dev-ai
11
+ Keywords: log,tail,color,terminal,cli,devtools,tui
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: System :: Logging
24
+ Classifier: Topic :: Terminals
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Dynamic: license-file
30
+
31
+ <div align="center">
32
+
33
+ # 🎨 tailr
34
+
35
+ **Colorized live log viewer for your terminal.**
36
+
37
+ Tail files or piped stdin with automatic log-level and keyword highlighting.
38
+ Zero dependencies. Works in Python 3.8+.
39
+
40
+ [![PyPI](https://img.shields.io/pypi/v/tailr?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/tailr/)
41
+ [![Python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue)](#)
42
+ [![License](https://img.shields.io/badge/license-MIT-green)](#)
43
+ [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fdsk--dev--ai%2Ftailr-blue?logo=docker)](https://github.com/dsk-dev-ai/tailr/pkgs/container/tailr)
44
+
45
+ </div>
46
+
47
+ ---
48
+
49
+ ## Why tailr?
50
+
51
+ Logs are noisy. `tailr` makes them *readable* at a glance:
52
+
53
+ - **Level highlighting** — `INFO`, `WARN`, `ERROR`, `FATAL`, `DEBUG`, `TRACE` get distinct colors.
54
+ - **Keyword highlighting** — `error`, `failed`, `exception`, `traceback`, `panic` pop in bright red.
55
+ - **Timestamp coloring** — dates and times are dimmed so the signal stands out.
56
+ - **Live follow** — watch logs grow in real time, just like `tail -f`.
57
+ - **Filter on the fly** — regex grep, or filter by log level.
58
+ - **Zero dependencies** — pure Python stdlib. No install drama, no bloat.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pip install tailr
64
+ ```
65
+
66
+ Or run without installing:
67
+
68
+ ```bash
69
+ pipx run tailr --help
70
+ ```
71
+
72
+ Via Docker:
73
+
74
+ ```bash
75
+ docker run --rm ghcr.io/dsk-dev-ai/tailr --help
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ```bash
81
+ # Colorize a log file (last 10 lines)
82
+ tailr app.log
83
+
84
+ # Follow a growing log live
85
+ tailr -f app.log
86
+
87
+ # Follow with more history
88
+ tailr -f -n 200 app.log
89
+
90
+ # Filter by log level
91
+ tailr -l ERROR app.log
92
+
93
+ # Grep with a regex, live
94
+ tailr -f -g "kafka|redis" app.log
95
+
96
+ # Pipe anything in
97
+ docker logs -f app | tailr
98
+ kubectl logs -f pod | tailr
99
+ journalctl -f | tailr -l WARN
100
+ ```
101
+
102
+ ### Options
103
+
104
+ | Flag | Description |
105
+ |------|-------------|
106
+ | `-f, --follow` | Keep tailing appended lines (like `tail -f`) |
107
+ | `-n, --lines N` | Show N lines from the end (default 10) |
108
+ | `-g, --grep REGEX` | Only show lines matching a regex |
109
+ | `-l, --level LVL` | Only show lines with this level (`ERROR`, `WARN`, …) |
110
+ | `--version` | Print version |
111
+
112
+ ### Colors
113
+
114
+ ```
115
+ INFO -> blue
116
+ DEBUG -> gray
117
+ WARN -> orange
118
+ ERROR -> red
119
+ FATAL -> magenta
120
+ TRACE -> gray
121
+ keywords -> bright red + bold
122
+ timestamps-> dim
123
+ ```
124
+
125
+ Set `NO_COLOR=1` to disable colorization, or `FORCE_COLOR=1` to force it.
126
+
127
+ ## How it works
128
+
129
+ `tailr` streams lines and applies a lightweight ANSI colorizer powered by a few
130
+ small regexes — no external parsing libs, no heuristics beyond the log shape.
131
+ It respects `isatty()` to avoid spewing escape codes into pipelines.
132
+
133
+ ## Tests
134
+
135
+ ```bash
136
+ python -m pytest
137
+ ```
138
+
139
+ Tested on Python 3.8–3.12 via GitHub Actions.
140
+
141
+ ## Sponsor
142
+
143
+ Enjoy `tailr`? Keep it free and dependency-free by [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub 💜.
144
+
145
+ ---
146
+
147
+ <div align="center">
148
+
149
+ **tailr** · Colorized logs you can actually read.
150
+
151
+ </div>
@@ -0,0 +1,10 @@
1
+ tailr/__init__.py,sha256=srf3GG3A4yi8SvG34RD6SHyof9rXs_XCcHiafDwCjBs,64
2
+ tailr/__main__.py,sha256=Ct7BGwm8nmXazokS5S7W6ixL5unBRJ_6LQ73i770kPc,66
3
+ tailr/cli.py,sha256=37CBw9nfEIZpqphAP2kAYeUd443VTGJjeLk54Af8BYs,1897
4
+ tailr/core.py,sha256=nuIADej11XnuvXX81vs6aLmFEZG9AE1cOxcfsjxWVxs,5102
5
+ tailr-1.0.0.dist-info/licenses/LICENSE,sha256=EAxljeOlydi0CA5dhf2z72wEjAtq_9Ig44QyLQ7XWLM,1067
6
+ tailr-1.0.0.dist-info/METADATA,sha256=fGkG8vhquGjmNGJwTSrN-uFMtuM3yyXy8smgm7SwIgI,4157
7
+ tailr-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ tailr-1.0.0.dist-info/entry_points.txt,sha256=NgeMHOacNGnmazJ_bCyHP-Nt9LgvrZwIXoepKo3YASY,41
9
+ tailr-1.0.0.dist-info/top_level.txt,sha256=JMHg3kox6k0dNoy2fmdrDUAGuOElFxJF4DuVc3osNLs,6
10
+ tailr-1.0.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,2 @@
1
+ [console_scripts]
2
+ tailr = tailr.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsk-dev-ai
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
+ tailr