omlish 0.0.0.dev117__py3-none-any.whl → 0.0.0.dev119__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.
- omlish/__about__.py +2 -2
- omlish/collections/hasheq.py +0 -10
- omlish/fnpairs.py +1 -10
- omlish/formats/json/__init__.py +5 -0
- omlish/formats/json/literals.py +179 -0
- omlish/formats/json/render.py +2 -3
- omlish/formats/json/stream/render.py +1 -1
- omlish/formats/json/types.py +6 -0
- omlish/lang/classes/abstract.py +37 -14
- omlish/lang/imports.py +1 -1
- omlish/lang/maybes.py +10 -12
- omlish/lite/journald.py +163 -0
- omlish/lite/logs.py +6 -2
- omlish/logs/_abc.py +53 -0
- omlish/logs/handlers.py +1 -1
- omlish/specs/jmespath/ast.py +199 -60
- omlish/specs/jmespath/cli.py +43 -29
- omlish/specs/jmespath/functions.py +397 -274
- omlish/specs/jmespath/lexer.py +2 -2
- omlish/specs/jmespath/parser.py +169 -133
- omlish/specs/jmespath/scope.py +15 -11
- omlish/specs/jmespath/visitor.py +211 -137
- omlish/testing/pytest/plugins/pydevd.py +6 -6
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev119.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev119.dist-info}/RECORD +29 -26
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev119.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev119.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev119.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev117.dist-info → omlish-0.0.0.dev119.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
omlish/collections/hasheq.py
CHANGED
@@ -27,21 +27,11 @@ class HashEq(lang.Abstract, ta.Generic[K]):
|
|
27
27
|
raise NotImplementedError
|
28
28
|
|
29
29
|
|
30
|
-
@lang.unabstract_class([
|
31
|
-
('hash', '_hash'),
|
32
|
-
('eq', '_eq'),
|
33
|
-
])
|
34
30
|
@dc.dataclass(frozen=True)
|
35
31
|
class HashEq_(ta.Generic[K]): # noqa
|
36
32
|
hash: ta.Callable[[K], int]
|
37
33
|
eq: ta.Callable[[K, K], bool]
|
38
34
|
|
39
|
-
def _hash(self, k: K) -> int:
|
40
|
-
return self.hash(k)
|
41
|
-
|
42
|
-
def _eq(self, l: K, r: K) -> bool:
|
43
|
-
return self.eq(l, r)
|
44
|
-
|
45
35
|
|
46
36
|
class hash_eq(HashEq[K], lang.NotInstantiable, lang.Final): # noqa
|
47
37
|
"""Workaround for PEP 695 support."""
|
omlish/fnpairs.py
CHANGED
@@ -94,21 +94,12 @@ class FnPair(ta.Generic[F, T], abc.ABC):
|
|
94
94
|
##
|
95
95
|
|
96
96
|
|
97
|
-
@lang.unabstract_class([
|
98
|
-
('forward', '_forward'),
|
99
|
-
('backward', 'backward'),
|
100
|
-
])
|
97
|
+
@lang.unabstract_class(['forward', 'backward'])
|
101
98
|
@dc.dataclass(frozen=True)
|
102
99
|
class Simple(FnPair[F, T]):
|
103
100
|
forward: ta.Callable[[F], T] # type: ignore
|
104
101
|
backward: ta.Callable[[T], F] # type: ignore
|
105
102
|
|
106
|
-
def _forward(self, f: F) -> T:
|
107
|
-
return self.forward(f)
|
108
|
-
|
109
|
-
def _backward(self, t: T) -> F:
|
110
|
-
return self.backward(t)
|
111
|
-
|
112
103
|
|
113
104
|
of = Simple
|
114
105
|
|
omlish/formats/json/__init__.py
CHANGED
@@ -0,0 +1,179 @@
|
|
1
|
+
# MIT License
|
2
|
+
# ===========
|
3
|
+
#
|
4
|
+
# Copyright (c) 2006 Bob Ippolito
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
7
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
8
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
9
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
12
|
+
# Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
15
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
17
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
18
|
+
# https://github.com/simplejson/simplejson/blob/6932004966ab70ef47250a2b3152acd8c904e6b5/simplejson/scanner.py
|
19
|
+
import json
|
20
|
+
import re
|
21
|
+
import sys
|
22
|
+
import typing as ta
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
|
27
|
+
|
28
|
+
_FOUR_DIGIT_HEX_PAT = re.compile(r'^[0-9a-fA-F]{4}$')
|
29
|
+
|
30
|
+
|
31
|
+
def _scan_four_digit_hex(
|
32
|
+
s: str,
|
33
|
+
end: int,
|
34
|
+
):
|
35
|
+
"""Scan a four digit hex number from s[end:end + 4]"""
|
36
|
+
|
37
|
+
msg = 'Invalid \\uXXXX escape sequence'
|
38
|
+
esc = s[end: end + 4]
|
39
|
+
if not _FOUR_DIGIT_HEX_PAT.match(esc):
|
40
|
+
raise json.JSONDecodeError(msg, s, end - 2)
|
41
|
+
|
42
|
+
try:
|
43
|
+
return int(esc, 16), end + 4
|
44
|
+
except ValueError:
|
45
|
+
raise json.JSONDecodeError(msg, s, end - 2) from None
|
46
|
+
|
47
|
+
|
48
|
+
_STRING_CHUNK_PAT = re.compile(r'(.*?)(["\\\x00-\x1f])', re.VERBOSE | re.MULTILINE | re.DOTALL)
|
49
|
+
|
50
|
+
_BACKSLASH_MAP = {
|
51
|
+
'"': '"',
|
52
|
+
'\\': '\\',
|
53
|
+
'/': '/',
|
54
|
+
'b': '\b',
|
55
|
+
'f': '\f',
|
56
|
+
'n': '\n',
|
57
|
+
'r': '\r',
|
58
|
+
't': '\t',
|
59
|
+
}
|
60
|
+
|
61
|
+
|
62
|
+
def parse_string(
|
63
|
+
s: str,
|
64
|
+
idx: int = 0,
|
65
|
+
*,
|
66
|
+
strict: bool = True,
|
67
|
+
) -> tuple[str, int]:
|
68
|
+
"""
|
69
|
+
Scan the string s for a JSON string. Idx is the index of the quote that starts the JSON string. Unescapes all valid
|
70
|
+
JSON string escape sequences and raises ValueError on attempt to decode an invalid string. If strict is False then
|
71
|
+
literal control characters are allowed in the string.
|
72
|
+
|
73
|
+
Returns a tuple of the decoded string and the index of the character in s after the end quote.
|
74
|
+
"""
|
75
|
+
|
76
|
+
if s[idx] != '"':
|
77
|
+
raise json.JSONDecodeError('No opening string quotes at', s, idx)
|
78
|
+
|
79
|
+
chunks: list[str] = []
|
80
|
+
end = idx + 1
|
81
|
+
while True:
|
82
|
+
chunk = _STRING_CHUNK_PAT.match(s, end)
|
83
|
+
if chunk is None:
|
84
|
+
raise json.JSONDecodeError('Unterminated string starting at', s, idx)
|
85
|
+
|
86
|
+
prev_end = end
|
87
|
+
end = chunk.end()
|
88
|
+
content, terminator = chunk.groups()
|
89
|
+
# Content is contains zero or more unescaped string characters
|
90
|
+
if content:
|
91
|
+
chunks.append(content)
|
92
|
+
|
93
|
+
# Terminator is the end of string, a literal control character, or a backslash denoting that an escape sequence
|
94
|
+
# follows
|
95
|
+
if terminator == '"':
|
96
|
+
break
|
97
|
+
elif terminator != '\\':
|
98
|
+
if strict:
|
99
|
+
msg = 'Invalid control character %r at'
|
100
|
+
raise json.JSONDecodeError(msg, s, prev_end)
|
101
|
+
else:
|
102
|
+
chunks.append(terminator)
|
103
|
+
continue
|
104
|
+
|
105
|
+
try:
|
106
|
+
esc = s[end]
|
107
|
+
except IndexError:
|
108
|
+
raise json.JSONDecodeError('Unterminated string starting at', s, idx) from None
|
109
|
+
|
110
|
+
# If not a unicode escape sequence, must be in the lookup table
|
111
|
+
if esc != 'u':
|
112
|
+
try:
|
113
|
+
char = _BACKSLASH_MAP[esc]
|
114
|
+
except KeyError:
|
115
|
+
msg = 'Invalid \\X escape sequence %r'
|
116
|
+
raise json.JSONDecodeError(msg, s, end) from None
|
117
|
+
end += 1
|
118
|
+
|
119
|
+
else:
|
120
|
+
# Unicode escape sequence
|
121
|
+
uni, end = _scan_four_digit_hex(s, end + 1)
|
122
|
+
|
123
|
+
# Check for surrogate pair on UCS-4 systems Note that this will join high/low surrogate pairs but will also
|
124
|
+
# pass unpaired surrogates through
|
125
|
+
if (
|
126
|
+
sys.maxunicode > 65535 and
|
127
|
+
uni & 0xFC00 == 0xD800 and
|
128
|
+
s[end: end + 2] == '\\u'
|
129
|
+
):
|
130
|
+
uni2, end2 = _scan_four_digit_hex(s, end + 2)
|
131
|
+
if uni2 & 0xFC00 == 0xDC00:
|
132
|
+
uni = 0x10000 + (((uni - 0xD800) << 10) | (uni2 - 0xDC00))
|
133
|
+
end = end2
|
134
|
+
|
135
|
+
char = chr(uni)
|
136
|
+
|
137
|
+
# Append the unescaped character
|
138
|
+
chunks.append(char)
|
139
|
+
|
140
|
+
return ''.join(chunks), end
|
141
|
+
|
142
|
+
|
143
|
+
def try_parse_string(
|
144
|
+
s: str,
|
145
|
+
idx: int = 0,
|
146
|
+
*,
|
147
|
+
strict: bool = True,
|
148
|
+
) -> tuple[str, int] | None:
|
149
|
+
if not s or s[idx] != '"':
|
150
|
+
return None
|
151
|
+
|
152
|
+
return parse_string(s, idx, strict=strict)
|
153
|
+
|
154
|
+
|
155
|
+
##
|
156
|
+
|
157
|
+
|
158
|
+
_NUMBER_PAT = re.compile(
|
159
|
+
r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
|
160
|
+
(re.VERBOSE | re.MULTILINE | re.DOTALL),
|
161
|
+
)
|
162
|
+
|
163
|
+
|
164
|
+
def try_parse_number(
|
165
|
+
s: str,
|
166
|
+
idx: int = 0,
|
167
|
+
*,
|
168
|
+
parse_float: ta.Callable[[str], float] = float,
|
169
|
+
parse_int: ta.Callable[[str], int] = int,
|
170
|
+
) -> tuple[int | float, int] | None:
|
171
|
+
if (m := _NUMBER_PAT.match(s, idx)) is None:
|
172
|
+
return None
|
173
|
+
|
174
|
+
integer, frac, exp = m.groups()
|
175
|
+
if frac or exp:
|
176
|
+
res = parse_float(integer + (frac or '') + (exp or ''))
|
177
|
+
else:
|
178
|
+
res = parse_int(integer)
|
179
|
+
return res, m.end()
|
omlish/formats/json/render.py
CHANGED
@@ -6,12 +6,11 @@ import typing as ta
|
|
6
6
|
|
7
7
|
from ... import lang
|
8
8
|
from . import consts
|
9
|
+
from .types import SCALAR_TYPES
|
10
|
+
from .types import Scalar
|
9
11
|
|
10
12
|
|
11
13
|
I = ta.TypeVar('I')
|
12
|
-
Scalar: ta.TypeAlias = bool | int | float | str | None
|
13
|
-
|
14
|
-
SCALAR_TYPES: tuple[type, ...] = (bool, int, float, str, type(None))
|
15
14
|
|
16
15
|
MULTILINE_SEPARATORS = consts.Separators(',', ': ')
|
17
16
|
|
omlish/lang/classes/abstract.py
CHANGED
@@ -7,6 +7,10 @@ T = ta.TypeVar('T')
|
|
7
7
|
|
8
8
|
_DISABLE_CHECKS = False
|
9
9
|
|
10
|
+
_ABSTRACT_METHODS_ATTR = '__abstractmethods__'
|
11
|
+
_IS_ABSTRACT_METHOD_ATTR = '__isabstractmethod__'
|
12
|
+
_FORCE_ABSTRACT_ATTR = '__forceabstract__'
|
13
|
+
|
10
14
|
|
11
15
|
def make_abstract(obj: T) -> T:
|
12
16
|
if callable(obj):
|
@@ -29,13 +33,13 @@ class Abstract(abc.ABC): # noqa
|
|
29
33
|
def __forceabstract__(self):
|
30
34
|
raise TypeError
|
31
35
|
|
32
|
-
setattr(__forceabstract__,
|
36
|
+
setattr(__forceabstract__, _IS_ABSTRACT_METHOD_ATTR, True)
|
33
37
|
|
34
38
|
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
35
39
|
if Abstract in cls.__bases__:
|
36
|
-
cls
|
40
|
+
setattr(cls, _FORCE_ABSTRACT_ATTR, getattr(Abstract, _FORCE_ABSTRACT_ATTR))
|
37
41
|
else:
|
38
|
-
cls
|
42
|
+
setattr(cls, _FORCE_ABSTRACT_ATTR, False)
|
39
43
|
|
40
44
|
super().__init_subclass__(**kwargs)
|
41
45
|
|
@@ -43,7 +47,7 @@ class Abstract(abc.ABC): # noqa
|
|
43
47
|
ams = {a for a, o in cls.__dict__.items() if is_abstract_method(o)}
|
44
48
|
seen = set(cls.__dict__)
|
45
49
|
for b in cls.__bases__:
|
46
|
-
ams.update(set(getattr(b,
|
50
|
+
ams.update(set(getattr(b, _ABSTRACT_METHODS_ATTR, [])) - seen)
|
47
51
|
seen.update(dir(b))
|
48
52
|
if ams:
|
49
53
|
raise TypeError(
|
@@ -53,18 +57,18 @@ class Abstract(abc.ABC): # noqa
|
|
53
57
|
|
54
58
|
|
55
59
|
def is_abstract_method(obj: ta.Any) -> bool:
|
56
|
-
return bool(getattr(obj,
|
60
|
+
return bool(getattr(obj, _IS_ABSTRACT_METHOD_ATTR, False))
|
57
61
|
|
58
62
|
|
59
63
|
def is_abstract_class(obj: ta.Any) -> bool:
|
60
|
-
if bool(getattr(obj,
|
64
|
+
if bool(getattr(obj, _ABSTRACT_METHODS_ATTR, [])):
|
61
65
|
return True
|
62
66
|
if isinstance(obj, type):
|
63
67
|
if Abstract in obj.__bases__:
|
64
68
|
return True
|
65
69
|
if (
|
66
70
|
Abstract in obj.__mro__
|
67
|
-
and getattr(obj.__dict__.get(
|
71
|
+
and getattr(obj.__dict__.get(_FORCE_ABSTRACT_ATTR, None), _IS_ABSTRACT_METHOD_ATTR, False)
|
68
72
|
):
|
69
73
|
return True
|
70
74
|
return False
|
@@ -75,13 +79,32 @@ def is_abstract(obj: ta.Any) -> bool:
|
|
75
79
|
|
76
80
|
|
77
81
|
def unabstract_class(
|
78
|
-
|
79
|
-
) -> ta.Callable[[type[T]], type[T]]:
|
82
|
+
members: ta.Iterable[str | tuple[str, ta.Any]],
|
83
|
+
): # -> ta.Callable[[type[T]], type[T]]:
|
80
84
|
def inner(cls):
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
85
|
+
if isinstance(members, str):
|
86
|
+
raise TypeError(members)
|
87
|
+
|
88
|
+
ams = getattr(cls, _ABSTRACT_METHODS_ATTR)
|
89
|
+
|
90
|
+
names: set[str] = set()
|
91
|
+
for m in members:
|
92
|
+
if isinstance(m, str):
|
93
|
+
if m not in ams:
|
94
|
+
raise NameError(m)
|
95
|
+
getattr(cls, m)
|
96
|
+
names.add(m)
|
97
|
+
|
98
|
+
elif isinstance(m, tuple):
|
99
|
+
name, impl = m
|
100
|
+
if name not in ams:
|
101
|
+
raise NameError(name)
|
102
|
+
if isinstance(impl, str):
|
103
|
+
impl = getattr(cls, impl)
|
104
|
+
setattr(cls, name, impl)
|
105
|
+
names.add(name)
|
106
|
+
|
107
|
+
setattr(cls, _ABSTRACT_METHODS_ATTR, ams - names)
|
86
108
|
return cls
|
109
|
+
|
87
110
|
return inner
|
omlish/lang/imports.py
CHANGED
@@ -312,7 +312,7 @@ class _ProxyInit:
|
|
312
312
|
try:
|
313
313
|
mod = self._mods_by_pkgs[pkg]
|
314
314
|
except KeyError:
|
315
|
-
mod = importlib.import_module(pkg, package=self.
|
315
|
+
mod = importlib.import_module(pkg, package=self._name_package.package)
|
316
316
|
|
317
317
|
val = getattr(mod, attr)
|
318
318
|
|
omlish/lang/maybes.py
CHANGED
@@ -50,15 +50,15 @@ class Maybe(abc.ABC, ta.Generic[T]):
|
|
50
50
|
raise NotImplementedError
|
51
51
|
|
52
52
|
@abc.abstractmethod
|
53
|
-
def or_else(self, other: T) ->
|
53
|
+
def or_else(self, other: T) -> T:
|
54
54
|
raise NotImplementedError
|
55
55
|
|
56
56
|
@abc.abstractmethod
|
57
|
-
def or_else_get(self, supplier: ta.Callable[[], T]) ->
|
57
|
+
def or_else_get(self, supplier: ta.Callable[[], T]) -> T:
|
58
58
|
raise NotImplementedError
|
59
59
|
|
60
60
|
@abc.abstractmethod
|
61
|
-
def or_else_raise(self, exception_supplier: ta.Callable[[], Exception]) ->
|
61
|
+
def or_else_raise(self, exception_supplier: ta.Callable[[], Exception]) -> T:
|
62
62
|
raise NotImplementedError
|
63
63
|
|
64
64
|
|
@@ -98,9 +98,7 @@ class _Maybe(Maybe[T], tuple):
|
|
98
98
|
|
99
99
|
def map(self, mapper: ta.Callable[[T], U]) -> Maybe[U]:
|
100
100
|
if self:
|
101
|
-
|
102
|
-
if value is not None:
|
103
|
-
return just(value)
|
101
|
+
return just(mapper(self[0]))
|
104
102
|
return _empty # noqa
|
105
103
|
|
106
104
|
def flat_map(self, mapper: ta.Callable[[T], Maybe[U]]) -> Maybe[U]:
|
@@ -111,15 +109,15 @@ class _Maybe(Maybe[T], tuple):
|
|
111
109
|
return value
|
112
110
|
return _empty # noqa
|
113
111
|
|
114
|
-
def or_else(self, other: T) ->
|
115
|
-
return self if self else
|
112
|
+
def or_else(self, other: T) -> T:
|
113
|
+
return self.must() if self else other
|
116
114
|
|
117
|
-
def or_else_get(self, supplier: ta.Callable[[], T]) ->
|
118
|
-
return self if self else
|
115
|
+
def or_else_get(self, supplier: ta.Callable[[], T]) -> T:
|
116
|
+
return self.must() if self else supplier()
|
119
117
|
|
120
|
-
def or_else_raise(self, exception_supplier: ta.Callable[[], Exception]) ->
|
118
|
+
def or_else_raise(self, exception_supplier: ta.Callable[[], Exception]) -> T:
|
121
119
|
if self:
|
122
|
-
return self
|
120
|
+
return self.must()
|
123
121
|
raise exception_supplier()
|
124
122
|
|
125
123
|
|
omlish/lite/journald.py
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# ruff: noqa: UP007 UP012
|
2
|
+
import ctypes as ct
|
3
|
+
import logging
|
4
|
+
import sys
|
5
|
+
import syslog
|
6
|
+
import threading
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from .cached import cached_nullary
|
10
|
+
|
11
|
+
|
12
|
+
##
|
13
|
+
|
14
|
+
|
15
|
+
class sd_iovec(ct.Structure): # noqa
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
sd_iovec._fields_ = [
|
20
|
+
('iov_base', ct.c_void_p), # Pointer to data.
|
21
|
+
('iov_len', ct.c_size_t), # Length of data.
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
|
27
|
+
|
28
|
+
@cached_nullary
|
29
|
+
def sd_libsystemd() -> ta.Any:
|
30
|
+
lib = ct.CDLL('libsystemd.so.0')
|
31
|
+
|
32
|
+
lib.sd_journal_sendv = lib['sd_journal_sendv'] # type: ignore
|
33
|
+
lib.sd_journal_sendv.restype = ct.c_int
|
34
|
+
lib.sd_journal_sendv.argtypes = [ct.POINTER(sd_iovec), ct.c_int]
|
35
|
+
|
36
|
+
return lib
|
37
|
+
|
38
|
+
|
39
|
+
@cached_nullary
|
40
|
+
def sd_try_libsystemd() -> ta.Optional[ta.Any]:
|
41
|
+
try:
|
42
|
+
return sd_libsystemd()
|
43
|
+
except OSError: # noqa
|
44
|
+
return None
|
45
|
+
|
46
|
+
|
47
|
+
##
|
48
|
+
|
49
|
+
|
50
|
+
def sd_journald_send(**fields: str) -> int:
|
51
|
+
lib = sd_libsystemd()
|
52
|
+
|
53
|
+
msgs = [
|
54
|
+
f'{k.upper()}={v}\0'.encode('utf-8')
|
55
|
+
for k, v in fields.items()
|
56
|
+
]
|
57
|
+
|
58
|
+
vec = (sd_iovec * len(msgs))()
|
59
|
+
cl = (ct.c_char_p * len(msgs))() # noqa
|
60
|
+
for i in range(len(msgs)):
|
61
|
+
vec[i].iov_base = ct.cast(ct.c_char_p(msgs[i]), ct.c_void_p)
|
62
|
+
vec[i].iov_len = len(msgs[i]) - 1
|
63
|
+
|
64
|
+
return lib.sd_journal_sendv(vec, len(msgs))
|
65
|
+
|
66
|
+
|
67
|
+
##
|
68
|
+
|
69
|
+
|
70
|
+
SD_LOG_LEVEL_MAP: ta.Mapping[int, int] = {
|
71
|
+
logging.FATAL: syslog.LOG_EMERG, # system is unusable
|
72
|
+
# LOG_ALERT ? # action must be taken immediately
|
73
|
+
logging.CRITICAL: syslog.LOG_CRIT,
|
74
|
+
logging.ERROR: syslog.LOG_ERR,
|
75
|
+
logging.WARNING: syslog.LOG_WARNING,
|
76
|
+
# LOG_NOTICE ? # normal but significant condition
|
77
|
+
logging.INFO: syslog.LOG_INFO,
|
78
|
+
logging.DEBUG: syslog.LOG_DEBUG,
|
79
|
+
}
|
80
|
+
|
81
|
+
|
82
|
+
class JournaldLogHandler(logging.Handler):
|
83
|
+
"""
|
84
|
+
TODO:
|
85
|
+
- fallback handler for when this barfs
|
86
|
+
"""
|
87
|
+
|
88
|
+
def __init__(
|
89
|
+
self,
|
90
|
+
*,
|
91
|
+
use_formatter_output: bool = False,
|
92
|
+
) -> None:
|
93
|
+
super().__init__()
|
94
|
+
|
95
|
+
sd_libsystemd()
|
96
|
+
|
97
|
+
self._use_formatter_output = use_formatter_output
|
98
|
+
|
99
|
+
#
|
100
|
+
|
101
|
+
EXTRA_RECORD_ATTRS_BY_JOURNALD_FIELD: ta.ClassVar[ta.Mapping[str, str]] = {
|
102
|
+
'name': 'name',
|
103
|
+
'module': 'module',
|
104
|
+
'exception': 'exc_text',
|
105
|
+
'thread_name': 'threadName',
|
106
|
+
'task_name': 'taskName',
|
107
|
+
}
|
108
|
+
|
109
|
+
def make_fields(self, record: logging.LogRecord) -> ta.Mapping[str, str]:
|
110
|
+
formatter_message = self.format(record)
|
111
|
+
if self._use_formatter_output:
|
112
|
+
message = formatter_message
|
113
|
+
else:
|
114
|
+
message = record.message
|
115
|
+
|
116
|
+
fields: dict[str, str] = {
|
117
|
+
'message': message,
|
118
|
+
'priority': str(SD_LOG_LEVEL_MAP[record.levelno]),
|
119
|
+
'tid': str(threading.get_ident()),
|
120
|
+
}
|
121
|
+
|
122
|
+
if (pathname := record.pathname) is not None:
|
123
|
+
fields['code_file'] = pathname
|
124
|
+
if (lineno := record.lineno) is not None:
|
125
|
+
fields['code_lineno'] = str(lineno)
|
126
|
+
if (func_name := record.funcName) is not None:
|
127
|
+
fields['code_func'] = func_name
|
128
|
+
|
129
|
+
for f, a in self.EXTRA_RECORD_ATTRS_BY_JOURNALD_FIELD.items():
|
130
|
+
if (v := getattr(record, a, None)) is not None:
|
131
|
+
fields[f] = str(v)
|
132
|
+
|
133
|
+
return fields
|
134
|
+
|
135
|
+
#
|
136
|
+
|
137
|
+
def emit(self, record: logging.LogRecord) -> None:
|
138
|
+
try:
|
139
|
+
fields = self.make_fields(record)
|
140
|
+
|
141
|
+
if rc := sd_journald_send(**fields):
|
142
|
+
raise RuntimeError(f'{self.__class__.__name__}.emit failed: {rc=}') # noqa
|
143
|
+
|
144
|
+
except RecursionError: # See issue 36272
|
145
|
+
raise
|
146
|
+
|
147
|
+
except Exception: # noqa
|
148
|
+
self.handleError(record)
|
149
|
+
|
150
|
+
|
151
|
+
def journald_log_handler_factory(
|
152
|
+
*,
|
153
|
+
no_tty_check: bool = False,
|
154
|
+
no_fallback: bool = False,
|
155
|
+
) -> logging.Handler:
|
156
|
+
if (
|
157
|
+
sys.platform == 'linux' and
|
158
|
+
(no_tty_check or not sys.stderr.isatty()) and
|
159
|
+
(no_fallback or sd_try_libsystemd() is not None)
|
160
|
+
):
|
161
|
+
return JournaldLogHandler()
|
162
|
+
|
163
|
+
return logging.StreamHandler()
|
omlish/lite/logs.py
CHANGED
@@ -91,7 +91,7 @@ class StandardLogFormatter(logging.Formatter):
|
|
91
91
|
if datefmt:
|
92
92
|
return ct.strftime(datefmt) # noqa
|
93
93
|
else:
|
94
|
-
t = ct.strftime(
|
94
|
+
t = ct.strftime('%Y-%m-%d %H:%M:%S')
|
95
95
|
return '%s.%03d' % (t, record.msecs)
|
96
96
|
|
97
97
|
|
@@ -228,6 +228,7 @@ def configure_standard_logging(
|
|
228
228
|
json: bool = False,
|
229
229
|
target: ta.Optional[logging.Logger] = None,
|
230
230
|
force: bool = False,
|
231
|
+
handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
|
231
232
|
) -> ta.Optional[StandardLogHandler]:
|
232
233
|
with _locking_logging_module_lock():
|
233
234
|
if target is None:
|
@@ -241,7 +242,10 @@ def configure_standard_logging(
|
|
241
242
|
|
242
243
|
#
|
243
244
|
|
244
|
-
|
245
|
+
if handler_factory is not None:
|
246
|
+
handler = handler_factory()
|
247
|
+
else:
|
248
|
+
handler = logging.StreamHandler()
|
245
249
|
|
246
250
|
#
|
247
251
|
|