nyxcore 0.1.0__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.
- nyxcore-0.1.0/PKG-INFO +153 -0
- nyxcore-0.1.0/README.md +142 -0
- nyxcore-0.1.0/pyproject.toml +24 -0
- nyxcore-0.1.0/src/nyxcore/__init__.py +0 -0
- nyxcore-0.1.0/src/nyxcore/color.py +160 -0
- nyxcore-0.1.0/src/nyxcore/logger/__init__.py +106 -0
nyxcore-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: nyxcore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: nyx's utility library
|
|
5
|
+
Author: verticalsync
|
|
6
|
+
Author-email: verticalsync <nightly@riseup.net>
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Project-URL: Homepage, https://github.com/verticalsync/nyxcore
|
|
9
|
+
Project-URL: Source, https://github.com/verticalsync/nyxcore
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# nyxcore
|
|
13
|
+
|
|
14
|
+
personal utility library
|
|
15
|
+
|
|
16
|
+
## modules
|
|
17
|
+
|
|
18
|
+
| module | description |
|
|
19
|
+
|---|---|
|
|
20
|
+
| [`nyxcore.color`](#nyxcorecolor) | ANSI color support — named colors, 256-color, true color, Windows compat, auto-strip |
|
|
21
|
+
| [`nyxcore.logger`](#nyxcorelogger) | Configurable logger with colored console output, file logging, custom formats |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## `nyxcore.color`
|
|
26
|
+
|
|
27
|
+
ANSI escape code helpers. Zero dependencies.
|
|
28
|
+
|
|
29
|
+
### colors
|
|
30
|
+
|
|
31
|
+
| code | `fg` | `bg` |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 30/40 | `fg.black` | `bg.black` |
|
|
34
|
+
| 31/41 | `fg.red` | `bg.red` |
|
|
35
|
+
| 32/42 | `fg.green` | `bg.green` |
|
|
36
|
+
| 33/43 | `fg.yellow` | `bg.yellow` |
|
|
37
|
+
| 34/44 | `fg.blue` | `bg.blue` |
|
|
38
|
+
| 35/45 | `fg.magenta` | `bg.magenta` |
|
|
39
|
+
| 36/46 | `fg.cyan` | `bg.cyan` |
|
|
40
|
+
| 37/47 | `fg.white` | `bg.white` |
|
|
41
|
+
| 90/100 | `fg.gray` | `bg.gray` |
|
|
42
|
+
| 91/101 | `fg.bright_red` | `bg.bright_red` |
|
|
43
|
+
| 92/102 | `fg.bright_green` | `bg.bright_green` |
|
|
44
|
+
| 93/103 | `fg.bright_yellow` | `bg.bright_yellow` |
|
|
45
|
+
| 94/104 | `fg.bright_blue` | `bg.bright_blue` |
|
|
46
|
+
| 95/105 | `fg.bright_magenta` | `bg.bright_magenta` |
|
|
47
|
+
| 96/106 | `fg.bright_cyan` | `bg.bright_cyan` |
|
|
48
|
+
| 97/107 | `fg.bright_white` | `bg.bright_white` |
|
|
49
|
+
| 39/49 | `fg.reset` | `bg.reset` |
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
print(f"{fg.cyan}hello{fg.reset}")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### `attr` — text styles
|
|
56
|
+
|
|
57
|
+
`attr.bold`, `attr.dim`, `attr.italic`, `attr.underline`, `attr.blink`, `attr.reverse`, `attr.hidden`, `attr.strikethrough`, `attr.reset`
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
print(f"{attr.bold}{fg.red}bold red{attr.reset}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### extended colors
|
|
64
|
+
|
|
65
|
+
| function | description |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `fg256(code)` | 256-color foreground (0–255) |
|
|
68
|
+
| `bg256(code)` | 256-color background |
|
|
69
|
+
| `fgrgb(r, g, b)` | 24-bit true color foreground |
|
|
70
|
+
| `bgrgb(r, g, b)` | 24-bit true color background |
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
print(f"{fg256(196)}bright red{fg.reset}")
|
|
74
|
+
print(f"{fgrgb(255, 100, 50)}orange{fg.reset}")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `strip(text)`
|
|
78
|
+
|
|
79
|
+
Remove all ANSI escape sequences.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
clean = strip("\x1b[31mhello\x1b[0m") # "hello"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `init(strip_auto=True)`
|
|
86
|
+
|
|
87
|
+
Enable Windows console ANSI support. Call once at startup.
|
|
88
|
+
|
|
89
|
+
On Windows: enables virtual terminal processing via Win32 API.
|
|
90
|
+
When `strip_auto=True`: non-TTY streams (piped/redirected) are wrapped to strip ANSI codes automatically.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from nyxcore.color import init
|
|
94
|
+
init()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## `nyxcore.logger`
|
|
100
|
+
|
|
101
|
+
Configurable logger on top of stdlib `logging`. Supports colored console output, file output, custom formats.
|
|
102
|
+
|
|
103
|
+
### `Logger(name, level, *, colorize, fmt, file)`
|
|
104
|
+
|
|
105
|
+
| param | type | default | description |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `name` | `str` | `"nyxcore"` | logger name |
|
|
108
|
+
| `level` | `str` or `int` | `"INFO"` | `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL` or a `logging` constant |
|
|
109
|
+
| `colorize` | `bool` or `None` | auto | `True` = colors on, `False` = plain, `None` = TTY-detect |
|
|
110
|
+
| `fmt` | `str` or `None` | default | see [format](#format) |
|
|
111
|
+
| `file` | `str` or `Path` | `None` | path to a log file (appends) |
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from nyxcore.logger import Logger
|
|
115
|
+
|
|
116
|
+
log = Logger("myapp")
|
|
117
|
+
log.info("hello")
|
|
118
|
+
log.warning("careful")
|
|
119
|
+
log.error("oops")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `set_level(level)`
|
|
123
|
+
|
|
124
|
+
Change minimum log level at runtime.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
log.set_level("DEBUG")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `add_file(path, level=None)`
|
|
131
|
+
|
|
132
|
+
Add a file handler after construction. File output is never colorized.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
log.add_file("app.log")
|
|
136
|
+
log.add_file("errors.log", level="ERROR")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### format
|
|
140
|
+
|
|
141
|
+
Placeholders: `{time}`, `{level}`, `{name}`, `{message}`. Python format specifiers work (`{level:^8}` centers in 8 chars).
|
|
142
|
+
|
|
143
|
+
Default:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
[{time}] {level:^8} | {name} | {message}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Custom:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
log = Logger("app", fmt="{level} | {message}")
|
|
153
|
+
```
|
nyxcore-0.1.0/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# nyxcore
|
|
2
|
+
|
|
3
|
+
personal utility library
|
|
4
|
+
|
|
5
|
+
## modules
|
|
6
|
+
|
|
7
|
+
| module | description |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [`nyxcore.color`](#nyxcorecolor) | ANSI color support — named colors, 256-color, true color, Windows compat, auto-strip |
|
|
10
|
+
| [`nyxcore.logger`](#nyxcorelogger) | Configurable logger with colored console output, file logging, custom formats |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## `nyxcore.color`
|
|
15
|
+
|
|
16
|
+
ANSI escape code helpers. Zero dependencies.
|
|
17
|
+
|
|
18
|
+
### colors
|
|
19
|
+
|
|
20
|
+
| code | `fg` | `bg` |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 30/40 | `fg.black` | `bg.black` |
|
|
23
|
+
| 31/41 | `fg.red` | `bg.red` |
|
|
24
|
+
| 32/42 | `fg.green` | `bg.green` |
|
|
25
|
+
| 33/43 | `fg.yellow` | `bg.yellow` |
|
|
26
|
+
| 34/44 | `fg.blue` | `bg.blue` |
|
|
27
|
+
| 35/45 | `fg.magenta` | `bg.magenta` |
|
|
28
|
+
| 36/46 | `fg.cyan` | `bg.cyan` |
|
|
29
|
+
| 37/47 | `fg.white` | `bg.white` |
|
|
30
|
+
| 90/100 | `fg.gray` | `bg.gray` |
|
|
31
|
+
| 91/101 | `fg.bright_red` | `bg.bright_red` |
|
|
32
|
+
| 92/102 | `fg.bright_green` | `bg.bright_green` |
|
|
33
|
+
| 93/103 | `fg.bright_yellow` | `bg.bright_yellow` |
|
|
34
|
+
| 94/104 | `fg.bright_blue` | `bg.bright_blue` |
|
|
35
|
+
| 95/105 | `fg.bright_magenta` | `bg.bright_magenta` |
|
|
36
|
+
| 96/106 | `fg.bright_cyan` | `bg.bright_cyan` |
|
|
37
|
+
| 97/107 | `fg.bright_white` | `bg.bright_white` |
|
|
38
|
+
| 39/49 | `fg.reset` | `bg.reset` |
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
print(f"{fg.cyan}hello{fg.reset}")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `attr` — text styles
|
|
45
|
+
|
|
46
|
+
`attr.bold`, `attr.dim`, `attr.italic`, `attr.underline`, `attr.blink`, `attr.reverse`, `attr.hidden`, `attr.strikethrough`, `attr.reset`
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
print(f"{attr.bold}{fg.red}bold red{attr.reset}")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### extended colors
|
|
53
|
+
|
|
54
|
+
| function | description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `fg256(code)` | 256-color foreground (0–255) |
|
|
57
|
+
| `bg256(code)` | 256-color background |
|
|
58
|
+
| `fgrgb(r, g, b)` | 24-bit true color foreground |
|
|
59
|
+
| `bgrgb(r, g, b)` | 24-bit true color background |
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
print(f"{fg256(196)}bright red{fg.reset}")
|
|
63
|
+
print(f"{fgrgb(255, 100, 50)}orange{fg.reset}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `strip(text)`
|
|
67
|
+
|
|
68
|
+
Remove all ANSI escape sequences.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
clean = strip("\x1b[31mhello\x1b[0m") # "hello"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `init(strip_auto=True)`
|
|
75
|
+
|
|
76
|
+
Enable Windows console ANSI support. Call once at startup.
|
|
77
|
+
|
|
78
|
+
On Windows: enables virtual terminal processing via Win32 API.
|
|
79
|
+
When `strip_auto=True`: non-TTY streams (piped/redirected) are wrapped to strip ANSI codes automatically.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from nyxcore.color import init
|
|
83
|
+
init()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## `nyxcore.logger`
|
|
89
|
+
|
|
90
|
+
Configurable logger on top of stdlib `logging`. Supports colored console output, file output, custom formats.
|
|
91
|
+
|
|
92
|
+
### `Logger(name, level, *, colorize, fmt, file)`
|
|
93
|
+
|
|
94
|
+
| param | type | default | description |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `name` | `str` | `"nyxcore"` | logger name |
|
|
97
|
+
| `level` | `str` or `int` | `"INFO"` | `DEBUG`/`INFO`/`WARNING`/`ERROR`/`CRITICAL` or a `logging` constant |
|
|
98
|
+
| `colorize` | `bool` or `None` | auto | `True` = colors on, `False` = plain, `None` = TTY-detect |
|
|
99
|
+
| `fmt` | `str` or `None` | default | see [format](#format) |
|
|
100
|
+
| `file` | `str` or `Path` | `None` | path to a log file (appends) |
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from nyxcore.logger import Logger
|
|
104
|
+
|
|
105
|
+
log = Logger("myapp")
|
|
106
|
+
log.info("hello")
|
|
107
|
+
log.warning("careful")
|
|
108
|
+
log.error("oops")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `set_level(level)`
|
|
112
|
+
|
|
113
|
+
Change minimum log level at runtime.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
log.set_level("DEBUG")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `add_file(path, level=None)`
|
|
120
|
+
|
|
121
|
+
Add a file handler after construction. File output is never colorized.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
log.add_file("app.log")
|
|
125
|
+
log.add_file("errors.log", level="ERROR")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### format
|
|
129
|
+
|
|
130
|
+
Placeholders: `{time}`, `{level}`, `{name}`, `{message}`. Python format specifiers work (`{level:^8}` centers in 8 chars).
|
|
131
|
+
|
|
132
|
+
Default:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
[{time}] {level:^8} | {name} | {message}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Custom:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
log = Logger("app", fmt="{level} | {message}")
|
|
142
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nyxcore"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "nyx's utility library"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "verticalsync", email = "nightly@riseup.net" }]
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
urls = { Homepage = "https://github.com/verticalsync/nyxcore", Source = "https://github.com/verticalsync/nyxcore" }
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["uv_build>=0.11.29,<0.12.0"]
|
|
13
|
+
build-backend = "uv_build"
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = ["isort>=8.0.1", "mypy>=1.15.0", "pytest>=8.0.0", "ruff>=0.15.22"]
|
|
17
|
+
|
|
18
|
+
[tool.pytest.ini_options]
|
|
19
|
+
testpaths = ["tests"]
|
|
20
|
+
|
|
21
|
+
[tool.mypy]
|
|
22
|
+
python_version = "3.14"
|
|
23
|
+
strict = true
|
|
24
|
+
ignore_missing_imports = true
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations # noqa: I001
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_ANSIRE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
9
|
+
|
|
10
|
+
class fg:
|
|
11
|
+
"""Foreground color escape codes."""
|
|
12
|
+
|
|
13
|
+
black = "\x1b[30m"
|
|
14
|
+
red = "\x1b[31m"
|
|
15
|
+
green = "\x1b[32m"
|
|
16
|
+
yellow = "\x1b[33m"
|
|
17
|
+
blue = "\x1b[34m"
|
|
18
|
+
magenta = "\x1b[35m"
|
|
19
|
+
cyan = "\x1b[36m"
|
|
20
|
+
white = "\x1b[37m"
|
|
21
|
+
gray = "\x1b[90m"
|
|
22
|
+
bright_red = "\x1b[91m"
|
|
23
|
+
bright_green = "\x1b[92m"
|
|
24
|
+
bright_yellow = "\x1b[93m"
|
|
25
|
+
bright_blue = "\x1b[94m"
|
|
26
|
+
bright_magenta = "\x1b[95m"
|
|
27
|
+
bright_cyan = "\x1b[96m"
|
|
28
|
+
bright_white = "\x1b[97m"
|
|
29
|
+
reset = "\x1b[39m"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class bg:
|
|
33
|
+
"""Background color escape codes."""
|
|
34
|
+
|
|
35
|
+
black = "\x1b[40m"
|
|
36
|
+
red = "\x1b[41m"
|
|
37
|
+
green = "\x1b[42m"
|
|
38
|
+
yellow = "\x1b[43m"
|
|
39
|
+
blue = "\x1b[44m"
|
|
40
|
+
magenta = "\x1b[45m"
|
|
41
|
+
cyan = "\x1b[46m"
|
|
42
|
+
white = "\x1b[47m"
|
|
43
|
+
gray = "\x1b[100m"
|
|
44
|
+
bright_red = "\x1b[101m"
|
|
45
|
+
bright_green = "\x1b[102m"
|
|
46
|
+
bright_yellow = "\x1b[103m"
|
|
47
|
+
bright_blue = "\x1b[104m"
|
|
48
|
+
bright_magenta = "\x1b[105m"
|
|
49
|
+
bright_cyan = "\x1b[106m"
|
|
50
|
+
bright_white = "\x1b[107m"
|
|
51
|
+
reset = "\x1b[49m"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class attr:
|
|
55
|
+
"""Text attribute/style escape codes."""
|
|
56
|
+
|
|
57
|
+
bold = "\x1b[1m"
|
|
58
|
+
dim = "\x1b[2m"
|
|
59
|
+
italic = "\x1b[3m"
|
|
60
|
+
underline = "\x1b[4m"
|
|
61
|
+
blink = "\x1b[5m"
|
|
62
|
+
reverse = "\x1b[7m"
|
|
63
|
+
hidden = "\x1b[8m"
|
|
64
|
+
strikethrough = "\x1b[9m"
|
|
65
|
+
reset = "\x1b[0m"
|
|
66
|
+
reset_bold = "\x1b[22m"
|
|
67
|
+
reset_italic = "\x1b[23m"
|
|
68
|
+
reset_underline = "\x1b[24m"
|
|
69
|
+
reset_blink = "\x1b[25m"
|
|
70
|
+
reset_reverse = "\x1b[27m"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fg256(code: int) -> str:
|
|
74
|
+
"""Generate a 256-color foreground escape sequence."""
|
|
75
|
+
return f"\x1b[38;5;{code}m"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def bg256(code: int) -> str:
|
|
79
|
+
"""Generate a 256-color background escape sequence."""
|
|
80
|
+
return f"\x1b[48;5;{code}m"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fgrgb(r: int, g: int, b: int) -> str:
|
|
84
|
+
"""Generate a 24-bit true-color foreground escape sequence."""
|
|
85
|
+
return f"\x1b[38;2;{r};{g};{b}m"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def bgrgb(r: int, g: int, b: int) -> str:
|
|
89
|
+
"""Generate a 24-bit true-color background escape sequence."""
|
|
90
|
+
return f"\x1b[48;2;{r};{g};{b}m"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def strip(text: str) -> str:
|
|
94
|
+
"""Remove all ANSI escape sequences from *text*."""
|
|
95
|
+
return _ANSIRE.sub("", text)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _init_windows() -> bool:
|
|
99
|
+
"""Enable virtual terminal processing on the Windows console."""
|
|
100
|
+
import ctypes
|
|
101
|
+
from ctypes import wintypes
|
|
102
|
+
|
|
103
|
+
STD_OUTPUT_HANDLE = -11
|
|
104
|
+
STD_ERROR_HANDLE = -12
|
|
105
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
|
|
106
|
+
DISABLE_NEWLINE_AUTO_RETURN = 8
|
|
107
|
+
|
|
108
|
+
kernel32 = ctypes.windll.kernel32
|
|
109
|
+
|
|
110
|
+
for handle in (STD_OUTPUT_HANDLE, STD_ERROR_HANDLE):
|
|
111
|
+
h = kernel32.GetStdHandle(handle)
|
|
112
|
+
if h == wintypes.HANDLE(-1).value or h is None:
|
|
113
|
+
continue
|
|
114
|
+
mode = wintypes.DWORD()
|
|
115
|
+
if not kernel32.GetConsoleMode(h, ctypes.byref(mode)):
|
|
116
|
+
continue
|
|
117
|
+
mode.value |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
118
|
+
mode.value |= DISABLE_NEWLINE_AUTO_RETURN
|
|
119
|
+
kernel32.SetConsoleMode(h, mode)
|
|
120
|
+
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _StreamWrapper:
|
|
125
|
+
"""Wraps a stream to strip ANSI codes on write."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, stream: Any) -> None:
|
|
128
|
+
self._stream = stream
|
|
129
|
+
|
|
130
|
+
def write(self, text: str) -> None:
|
|
131
|
+
"""Write *text* with ANSI codes stripped."""
|
|
132
|
+
self._stream.write(strip(text))
|
|
133
|
+
|
|
134
|
+
def flush(self) -> None:
|
|
135
|
+
"""Flush the underlying stream."""
|
|
136
|
+
self._stream.flush()
|
|
137
|
+
|
|
138
|
+
def __getattr__(self, name: str): # type: ignore[no-untyped-def]
|
|
139
|
+
return getattr(self._stream, name)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def init(*, strip_auto: bool = True) -> None:
|
|
143
|
+
"""Enable ANSI color support and configure auto-stripping.
|
|
144
|
+
|
|
145
|
+
On Windows this enables virtual terminal processing via the Win32 API.
|
|
146
|
+
When *strip_auto* is ``True`` (the default), non-TTY output streams
|
|
147
|
+
are wrapped to automatically strip ANSI codes so they don't appear
|
|
148
|
+
in redirected or file output.
|
|
149
|
+
"""
|
|
150
|
+
if os.name != "nt":
|
|
151
|
+
return
|
|
152
|
+
try:
|
|
153
|
+
_init_windows()
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
if strip_auto:
|
|
157
|
+
if not sys.stdout.isatty():
|
|
158
|
+
sys.stdout = _StreamWrapper(sys.stdout)
|
|
159
|
+
if not sys.stderr.isatty():
|
|
160
|
+
sys.stderr = _StreamWrapper(sys.stderr)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations # noqa: I001
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from nyxcore import color
|
|
10
|
+
|
|
11
|
+
Level = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
12
|
+
|
|
13
|
+
_LEVELS = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL}
|
|
14
|
+
|
|
15
|
+
_COLORS: dict[int, str] = {
|
|
16
|
+
logging.DEBUG: color.fg256(244),
|
|
17
|
+
logging.INFO: color.fg256(39),
|
|
18
|
+
logging.WARNING: color.fg256(220),
|
|
19
|
+
logging.ERROR: color.fg256(196),
|
|
20
|
+
logging.CRITICAL: f"{color.fg256(196)}{color.attr.bold}",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _levelno(name: str) -> int:
|
|
25
|
+
"""Convert a level string (case-insensitive) to a logging level constant."""
|
|
26
|
+
return _LEVELS[name.upper()]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Formatter(logging.Formatter):
|
|
30
|
+
"""Custom formatter that optionally wraps level/msg in ANSI color codes."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, fmt: str, colorize: bool) -> None:
|
|
33
|
+
super().__init__(fmt)
|
|
34
|
+
self.colorize = colorize
|
|
35
|
+
|
|
36
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
37
|
+
if self.colorize:
|
|
38
|
+
c = _COLORS.get(record.levelno, color.attr.reset)
|
|
39
|
+
record.msg = f"{c}{record.msg}{color.attr.reset}"
|
|
40
|
+
record.levelname = f"{c}{record.levelname}{color.attr.reset}"
|
|
41
|
+
return super().format(record)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_TO_LOGGING = {"time": "asctime", "level": "levelname", "name": "name", "message": "message"}
|
|
45
|
+
_FMT_PATTERN = re.compile(r"\{(\w+)(?::[^}]*)?\}")
|
|
46
|
+
|
|
47
|
+
class Logger:
|
|
48
|
+
"""Configurable logger with optional color and file output.
|
|
49
|
+
|
|
50
|
+
Wraps the stdlib ``logging`` module. Output can go to the terminal,
|
|
51
|
+
a file, or both simultaneously.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
name: str = "nyxcore",
|
|
57
|
+
level: Level | int = "INFO",
|
|
58
|
+
*,
|
|
59
|
+
colorize: bool | None = None,
|
|
60
|
+
fmt: str | None = None,
|
|
61
|
+
file: str | Path | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
if isinstance(level, str):
|
|
64
|
+
level = _levelno(level)
|
|
65
|
+
_use_color = sys.stdout.isatty() if colorize is None else colorize
|
|
66
|
+
|
|
67
|
+
if fmt is None:
|
|
68
|
+
fmt = "[{time}] {level:^8} | {name} | {message}"
|
|
69
|
+
self._fmt = _FMT_PATTERN.sub(lambda m: f"%({_TO_LOGGING[m.group(1)]})s", fmt)
|
|
70
|
+
|
|
71
|
+
self._logger = logging.getLogger(name)
|
|
72
|
+
self._logger.setLevel(level)
|
|
73
|
+
self._logger.handlers.clear()
|
|
74
|
+
|
|
75
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
76
|
+
handler.setLevel(level)
|
|
77
|
+
handler.setFormatter(_Formatter(self._fmt, _use_color))
|
|
78
|
+
self._logger.addHandler(handler)
|
|
79
|
+
|
|
80
|
+
if file is not None:
|
|
81
|
+
self.add_file(file, level)
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, name: str): # type: ignore[no-untyped-def]
|
|
84
|
+
"""Delegate attribute access to the underlying logger."""
|
|
85
|
+
return getattr(self._logger, name)
|
|
86
|
+
|
|
87
|
+
def set_level(self, level: Level | int) -> None:
|
|
88
|
+
"""Change the minimum log level at runtime."""
|
|
89
|
+
if isinstance(level, str):
|
|
90
|
+
level = _levelno(level)
|
|
91
|
+
self._logger.setLevel(level)
|
|
92
|
+
for h in self._logger.handlers:
|
|
93
|
+
h.setLevel(level)
|
|
94
|
+
|
|
95
|
+
def add_file(self, path: str | Path, level: Level | int | None = None) -> None:
|
|
96
|
+
"""Add a file handler for persistent logging."""
|
|
97
|
+
path = Path(path)
|
|
98
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
if isinstance(level, str):
|
|
100
|
+
level = _levelno(level)
|
|
101
|
+
if level is None:
|
|
102
|
+
level = self._logger.level
|
|
103
|
+
fh = logging.FileHandler(str(path), encoding="utf-8")
|
|
104
|
+
fh.setLevel(level)
|
|
105
|
+
fh.setFormatter(logging.Formatter(self._fmt))
|
|
106
|
+
self._logger.addHandler(fh)
|