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 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
+ ```
@@ -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)