ominfra 0.0.0.dev429__py3-none-any.whl → 0.0.0.dev431__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.
- ominfra/scripts/journald2aws.py +991 -690
- ominfra/scripts/manage.py +1030 -728
- ominfra/scripts/supervisor.py +1030 -728
- {ominfra-0.0.0.dev429.dist-info → ominfra-0.0.0.dev431.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev429.dist-info → ominfra-0.0.0.dev431.dist-info}/RECORD +9 -9
- {ominfra-0.0.0.dev429.dist-info → ominfra-0.0.0.dev431.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev429.dist-info → ominfra-0.0.0.dev431.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev429.dist-info → ominfra-0.0.0.dev431.dist-info}/licenses/LICENSE +0 -0
- {ominfra-0.0.0.dev429.dist-info → ominfra-0.0.0.dev431.dist-info}/top_level.txt +0 -0
ominfra/scripts/journald2aws.py
CHANGED
@@ -92,13 +92,15 @@ LogLevel = int # ta.TypeAlias
|
|
92
92
|
# ../../../../omlish/configs/formats.py
|
93
93
|
ConfigDataT = ta.TypeVar('ConfigDataT', bound='ConfigData')
|
94
94
|
|
95
|
-
# ../../../../omlish/logs/
|
95
|
+
# ../../../../omlish/logs/infos.py
|
96
|
+
LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
|
96
97
|
LoggingExcInfoTuple = ta.Tuple[ta.Type[BaseException], BaseException, ta.Optional[types.TracebackType]] # ta.TypeAlias
|
97
98
|
LoggingExcInfo = ta.Union[BaseException, LoggingExcInfoTuple] # ta.TypeAlias
|
98
99
|
LoggingExcInfoArg = ta.Union[LoggingExcInfo, bool, None] # ta.TypeAlias
|
100
|
+
LoggingContextInfo = ta.Any # ta.TypeAlias
|
99
101
|
|
100
|
-
# ../../../../omlish/logs/
|
101
|
-
|
102
|
+
# ../../../../omlish/logs/contexts.py
|
103
|
+
LoggingContextInfoT = ta.TypeVar('LoggingContextInfoT', bound=LoggingContextInfo)
|
102
104
|
|
103
105
|
# ../../../threadworkers.py
|
104
106
|
ThreadWorkerT = ta.TypeVar('ThreadWorkerT', bound='ThreadWorker')
|
@@ -1600,8 +1602,6 @@ class AttrOps(ta.Generic[T]):
|
|
1600
1602
|
self._eq = _eq
|
1601
1603
|
return _eq
|
1602
1604
|
|
1603
|
-
#
|
1604
|
-
|
1605
1605
|
@property
|
1606
1606
|
def hash_eq(self) -> ta.Tuple[
|
1607
1607
|
ta.Callable[[T], int],
|
@@ -1609,6 +1609,8 @@ class AttrOps(ta.Generic[T]):
|
|
1609
1609
|
]:
|
1610
1610
|
return (self.hash, self.eq)
|
1611
1611
|
|
1612
|
+
#
|
1613
|
+
|
1612
1614
|
@property
|
1613
1615
|
def repr_hash_eq(self) -> ta.Tuple[
|
1614
1616
|
ta.Callable[[T], str],
|
@@ -1619,20 +1621,25 @@ class AttrOps(ta.Generic[T]):
|
|
1619
1621
|
|
1620
1622
|
#
|
1621
1623
|
|
1624
|
+
class NOT_SET: # noqa
|
1625
|
+
def __new__(cls, *args, **kwargs): # noqa
|
1626
|
+
raise TypeError
|
1627
|
+
|
1622
1628
|
def install(
|
1623
1629
|
self,
|
1624
1630
|
locals_dct: ta.MutableMapping[str, ta.Any],
|
1625
1631
|
*,
|
1626
|
-
|
1627
|
-
|
1628
|
-
|
1629
|
-
eq: bool = False,
|
1632
|
+
repr: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
|
1633
|
+
hash: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET, # noqa
|
1634
|
+
eq: ta.Union[bool, ta.Type[NOT_SET]] = NOT_SET,
|
1630
1635
|
) -> 'AttrOps[T]':
|
1631
|
-
if repr
|
1636
|
+
if all(a is self.NOT_SET for a in (repr, hash, eq)):
|
1637
|
+
repr = hash = eq = True # noqa
|
1638
|
+
if repr:
|
1632
1639
|
locals_dct.update(__repr__=self.repr)
|
1633
|
-
if hash
|
1640
|
+
if hash:
|
1634
1641
|
locals_dct.update(__hash__=self.hash)
|
1635
|
-
if eq
|
1642
|
+
if eq:
|
1636
1643
|
locals_dct.update(__eq__=self.eq)
|
1637
1644
|
return self
|
1638
1645
|
|
@@ -2684,124 +2691,6 @@ def format_num_bytes(num_bytes: int) -> str:
|
|
2684
2691
|
return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
|
2685
2692
|
|
2686
2693
|
|
2687
|
-
########################################
|
2688
|
-
# ../../../../../omlish/logs/infos.py
|
2689
|
-
|
2690
|
-
|
2691
|
-
##
|
2692
|
-
|
2693
|
-
|
2694
|
-
def logging_context_info(cls):
|
2695
|
-
return cls
|
2696
|
-
|
2697
|
-
|
2698
|
-
##
|
2699
|
-
|
2700
|
-
|
2701
|
-
@logging_context_info
|
2702
|
-
@ta.final
|
2703
|
-
class LoggingSourceFileInfo(ta.NamedTuple):
|
2704
|
-
file_name: str
|
2705
|
-
module: str
|
2706
|
-
|
2707
|
-
@classmethod
|
2708
|
-
def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
|
2709
|
-
if file_path is None:
|
2710
|
-
return None
|
2711
|
-
|
2712
|
-
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
|
2713
|
-
try:
|
2714
|
-
file_name = os.path.basename(file_path)
|
2715
|
-
module = os.path.splitext(file_name)[0]
|
2716
|
-
except (TypeError, ValueError, AttributeError):
|
2717
|
-
return None
|
2718
|
-
|
2719
|
-
return cls(
|
2720
|
-
file_name,
|
2721
|
-
module,
|
2722
|
-
)
|
2723
|
-
|
2724
|
-
|
2725
|
-
##
|
2726
|
-
|
2727
|
-
|
2728
|
-
@logging_context_info
|
2729
|
-
@ta.final
|
2730
|
-
class LoggingThreadInfo(ta.NamedTuple):
|
2731
|
-
ident: int
|
2732
|
-
native_id: ta.Optional[int]
|
2733
|
-
name: str
|
2734
|
-
|
2735
|
-
@classmethod
|
2736
|
-
def build(cls) -> 'LoggingThreadInfo':
|
2737
|
-
return cls(
|
2738
|
-
threading.get_ident(),
|
2739
|
-
threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
|
2740
|
-
threading.current_thread().name,
|
2741
|
-
)
|
2742
|
-
|
2743
|
-
|
2744
|
-
##
|
2745
|
-
|
2746
|
-
|
2747
|
-
@logging_context_info
|
2748
|
-
@ta.final
|
2749
|
-
class LoggingProcessInfo(ta.NamedTuple):
|
2750
|
-
pid: int
|
2751
|
-
|
2752
|
-
@classmethod
|
2753
|
-
def build(cls) -> 'LoggingProcessInfo':
|
2754
|
-
return cls(
|
2755
|
-
os.getpid(),
|
2756
|
-
)
|
2757
|
-
|
2758
|
-
|
2759
|
-
##
|
2760
|
-
|
2761
|
-
|
2762
|
-
@logging_context_info
|
2763
|
-
@ta.final
|
2764
|
-
class LoggingMultiprocessingInfo(ta.NamedTuple):
|
2765
|
-
process_name: str
|
2766
|
-
|
2767
|
-
@classmethod
|
2768
|
-
def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
|
2769
|
-
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
|
2770
|
-
if (mp := sys.modules.get('multiprocessing')) is None:
|
2771
|
-
return None
|
2772
|
-
|
2773
|
-
return cls(
|
2774
|
-
mp.current_process().name,
|
2775
|
-
)
|
2776
|
-
|
2777
|
-
|
2778
|
-
##
|
2779
|
-
|
2780
|
-
|
2781
|
-
@logging_context_info
|
2782
|
-
@ta.final
|
2783
|
-
class LoggingAsyncioTaskInfo(ta.NamedTuple):
|
2784
|
-
name: str
|
2785
|
-
|
2786
|
-
@classmethod
|
2787
|
-
def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
|
2788
|
-
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
|
2789
|
-
if (asyncio := sys.modules.get('asyncio')) is None:
|
2790
|
-
return None
|
2791
|
-
|
2792
|
-
try:
|
2793
|
-
task = asyncio.current_task()
|
2794
|
-
except Exception: # noqa
|
2795
|
-
return None
|
2796
|
-
|
2797
|
-
if task is None:
|
2798
|
-
return None
|
2799
|
-
|
2800
|
-
return cls(
|
2801
|
-
task.get_name(), # Always non-None
|
2802
|
-
)
|
2803
|
-
|
2804
|
-
|
2805
2694
|
########################################
|
2806
2695
|
# ../../../../../omlish/logs/levels.py
|
2807
2696
|
|
@@ -2821,36 +2710,66 @@ class NamedLogLevel(int):
|
|
2821
2710
|
|
2822
2711
|
#
|
2823
2712
|
|
2824
|
-
|
2825
|
-
|
2826
|
-
|
2713
|
+
_CACHE: ta.ClassVar[ta.MutableMapping[int, 'NamedLogLevel']] = {}
|
2714
|
+
|
2715
|
+
@ta.overload
|
2716
|
+
def __new__(cls, name: str, offset: int = 0, /) -> 'NamedLogLevel':
|
2717
|
+
...
|
2718
|
+
|
2719
|
+
@ta.overload
|
2720
|
+
def __new__(cls, i: int, /) -> 'NamedLogLevel':
|
2721
|
+
...
|
2827
2722
|
|
2828
|
-
|
2723
|
+
def __new__(cls, x, offset=0, /):
|
2724
|
+
if isinstance(x, str):
|
2725
|
+
return cls(cls._INTS_BY_NAME[x.upper()] + offset)
|
2726
|
+
elif not offset and (c := cls._CACHE.get(x)) is not None:
|
2727
|
+
return c
|
2728
|
+
else:
|
2729
|
+
return super().__new__(cls, x + offset)
|
2730
|
+
|
2731
|
+
#
|
2732
|
+
|
2733
|
+
_name_and_offset: ta.Tuple[str, int]
|
2829
2734
|
|
2830
2735
|
@property
|
2831
|
-
def
|
2736
|
+
def name_and_offset(self) -> ta.Tuple[str, int]:
|
2832
2737
|
try:
|
2833
|
-
return self.
|
2738
|
+
return self._name_and_offset
|
2834
2739
|
except AttributeError:
|
2835
2740
|
pass
|
2836
2741
|
|
2837
|
-
if (n := self.
|
2742
|
+
if (n := self._NAMES_BY_INT.get(self)) is not None:
|
2743
|
+
t = (n, 0)
|
2744
|
+
else:
|
2838
2745
|
for n, i in self._NAME_INT_PAIRS: # noqa
|
2839
2746
|
if self >= i:
|
2747
|
+
t = (n, (self - i))
|
2840
2748
|
break
|
2841
2749
|
else:
|
2842
|
-
|
2750
|
+
t = ('NOTSET', int(self))
|
2751
|
+
|
2752
|
+
self._name_and_offset = t
|
2753
|
+
return t
|
2754
|
+
|
2755
|
+
@property
|
2756
|
+
def exact_name(self) -> ta.Optional[str]:
|
2757
|
+
n, o = self.name_and_offset
|
2758
|
+
return n if not o else None
|
2843
2759
|
|
2844
|
-
|
2760
|
+
@property
|
2761
|
+
def effective_name(self) -> str:
|
2762
|
+
n, _ = self.name_and_offset
|
2845
2763
|
return n
|
2846
2764
|
|
2847
2765
|
#
|
2848
2766
|
|
2849
|
-
def __repr__(self) -> str:
|
2850
|
-
return f'{self.__class__.__name__}({int(self)})'
|
2851
|
-
|
2852
2767
|
def __str__(self) -> str:
|
2853
|
-
return self.exact_name or f'{self.effective_name
|
2768
|
+
return self.exact_name or f'{self.effective_name}{int(self):+}'
|
2769
|
+
|
2770
|
+
def __repr__(self) -> str:
|
2771
|
+
n, o = self.name_and_offset
|
2772
|
+
return f'{self.__class__.__name__}({n!r}{f", {int(o)}" if o else ""})'
|
2854
2773
|
|
2855
2774
|
#
|
2856
2775
|
|
@@ -2870,6 +2789,9 @@ NamedLogLevel.DEBUG = NamedLogLevel(logging.DEBUG)
|
|
2870
2789
|
NamedLogLevel.NOTSET = NamedLogLevel(logging.NOTSET)
|
2871
2790
|
|
2872
2791
|
|
2792
|
+
NamedLogLevel._CACHE.update({i: NamedLogLevel(i) for i in NamedLogLevel._NAMES_BY_INT}) # noqa
|
2793
|
+
|
2794
|
+
|
2873
2795
|
########################################
|
2874
2796
|
# ../../../../../omlish/logs/std/filters.py
|
2875
2797
|
|
@@ -4907,74 +4829,362 @@ def check_lite_runtime_version() -> None:
|
|
4907
4829
|
|
4908
4830
|
|
4909
4831
|
########################################
|
4910
|
-
# ../../../../../omlish/logs/
|
4832
|
+
# ../../../../../omlish/logs/infos.py
|
4833
|
+
"""
|
4834
|
+
TODO:
|
4835
|
+
- remove redundant info fields only present for std adaptation (Level.name, ...)
|
4836
|
+
"""
|
4911
4837
|
|
4912
4838
|
|
4913
4839
|
##
|
4914
4840
|
|
4915
4841
|
|
4916
|
-
|
4842
|
+
def logging_context_info(cls):
|
4843
|
+
return cls
|
4844
|
+
|
4845
|
+
|
4917
4846
|
@ta.final
|
4918
|
-
class
|
4919
|
-
|
4920
|
-
|
4921
|
-
name: str
|
4922
|
-
stack_info: ta.Optional[str]
|
4847
|
+
class LoggingContextInfos:
|
4848
|
+
def __new__(cls, *args, **kwargs): # noqa
|
4849
|
+
raise TypeError
|
4923
4850
|
|
4924
|
-
|
4925
|
-
def is_internal_frame(cls, frame: types.FrameType) -> bool:
|
4926
|
-
file_path = os.path.normcase(frame.f_code.co_filename)
|
4851
|
+
#
|
4927
4852
|
|
4928
|
-
|
4929
|
-
|
4930
|
-
|
4931
|
-
|
4932
|
-
return True
|
4853
|
+
@logging_context_info
|
4854
|
+
@ta.final
|
4855
|
+
class Name(ta.NamedTuple):
|
4856
|
+
name: str
|
4933
4857
|
|
4934
|
-
|
4858
|
+
@logging_context_info
|
4859
|
+
@ta.final
|
4860
|
+
class Level(ta.NamedTuple):
|
4861
|
+
level: NamedLogLevel
|
4862
|
+
name: str
|
4935
4863
|
|
4936
|
-
|
4937
|
-
|
4938
|
-
|
4864
|
+
@classmethod
|
4865
|
+
def build(cls, level: int) -> 'LoggingContextInfos.Level':
|
4866
|
+
nl: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
|
4867
|
+
return cls(
|
4868
|
+
level=nl,
|
4869
|
+
name=logging.getLevelName(nl),
|
4870
|
+
)
|
4939
4871
|
|
4940
|
-
|
4941
|
-
|
4942
|
-
|
4943
|
-
|
4944
|
-
|
4872
|
+
@logging_context_info
|
4873
|
+
@ta.final
|
4874
|
+
class Msg(ta.NamedTuple):
|
4875
|
+
msg: str
|
4876
|
+
args: ta.Union[tuple, ta.Mapping[ta.Any, ta.Any], None]
|
4877
|
+
|
4878
|
+
@classmethod
|
4879
|
+
def build(
|
4880
|
+
cls,
|
4881
|
+
msg: ta.Union[str, tuple, LoggingMsgFn],
|
4882
|
+
*args: ta.Any,
|
4883
|
+
) -> 'LoggingContextInfos.Msg':
|
4884
|
+
s: str
|
4885
|
+
a: ta.Any
|
4886
|
+
|
4887
|
+
if callable(msg):
|
4888
|
+
if args:
|
4889
|
+
raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
|
4890
|
+
x = msg()
|
4891
|
+
if isinstance(x, str):
|
4892
|
+
s, a = x, ()
|
4893
|
+
elif isinstance(x, tuple):
|
4894
|
+
if x:
|
4895
|
+
s, a = x[0], x[1:]
|
4896
|
+
else:
|
4897
|
+
s, a = '', ()
|
4898
|
+
else:
|
4899
|
+
raise TypeError(x)
|
4945
4900
|
|
4946
|
-
|
4901
|
+
elif isinstance(msg, tuple):
|
4902
|
+
if args:
|
4903
|
+
raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
|
4904
|
+
if msg:
|
4905
|
+
s, a = msg[0], msg[1:]
|
4906
|
+
else:
|
4907
|
+
s, a = '', ()
|
4947
4908
|
|
4948
|
-
|
4909
|
+
elif isinstance(msg, str):
|
4910
|
+
s, a = msg, args
|
4911
|
+
|
4912
|
+
else:
|
4913
|
+
raise TypeError(msg)
|
4914
|
+
|
4915
|
+
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307 # noqa
|
4916
|
+
if a and len(a) == 1 and isinstance(a[0], collections.abc.Mapping) and a[0]:
|
4917
|
+
a = a[0]
|
4918
|
+
|
4919
|
+
return cls(
|
4920
|
+
msg=s,
|
4921
|
+
args=a,
|
4922
|
+
)
|
4923
|
+
|
4924
|
+
@logging_context_info
|
4925
|
+
@ta.final
|
4926
|
+
class Extra(ta.NamedTuple):
|
4927
|
+
extra: ta.Mapping[ta.Any, ta.Any]
|
4928
|
+
|
4929
|
+
@logging_context_info
|
4930
|
+
@ta.final
|
4931
|
+
class Time(ta.NamedTuple):
|
4932
|
+
ns: int
|
4933
|
+
secs: float
|
4934
|
+
msecs: float
|
4935
|
+
relative_secs: float
|
4936
|
+
|
4937
|
+
@classmethod
|
4938
|
+
def get_std_start_ns(cls) -> int:
|
4939
|
+
x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
|
4940
|
+
|
4941
|
+
# Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`,
|
4942
|
+
# an int.
|
4943
|
+
#
|
4944
|
+
# See:
|
4945
|
+
# - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
4946
|
+
#
|
4947
|
+
if isinstance(x, float):
|
4948
|
+
return int(x * 1e9)
|
4949
|
+
else:
|
4950
|
+
return x
|
4951
|
+
|
4952
|
+
@classmethod
|
4953
|
+
def build(
|
4954
|
+
cls,
|
4955
|
+
ns: int,
|
4956
|
+
*,
|
4957
|
+
start_ns: ta.Optional[int] = None,
|
4958
|
+
) -> 'LoggingContextInfos.Time':
|
4959
|
+
# https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
4960
|
+
secs = ns / 1e9 # ns to float seconds
|
4961
|
+
|
4962
|
+
# Get the number of whole milliseconds (0-999) in the fractional part of seconds.
|
4963
|
+
# Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
|
4964
|
+
# Convert to float by adding 0.0 for historical reasons. See gh-89047
|
4965
|
+
msecs = (ns % 1_000_000_000) // 1_000_000 + 0.0
|
4966
|
+
|
4967
|
+
# https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
|
4968
|
+
if msecs == 999.0 and int(secs) != ns // 1_000_000_000:
|
4969
|
+
# ns -> sec conversion can round up, e.g:
|
4970
|
+
# 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
|
4971
|
+
msecs = 0.0
|
4972
|
+
|
4973
|
+
if start_ns is None:
|
4974
|
+
start_ns = cls.get_std_start_ns()
|
4975
|
+
relative_secs = (ns - start_ns) / 1e6
|
4976
|
+
|
4977
|
+
return cls(
|
4978
|
+
ns=ns,
|
4979
|
+
secs=secs,
|
4980
|
+
msecs=msecs,
|
4981
|
+
relative_secs=relative_secs,
|
4982
|
+
)
|
4983
|
+
|
4984
|
+
@logging_context_info
|
4985
|
+
@ta.final
|
4986
|
+
class Exc(ta.NamedTuple):
|
4987
|
+
info: LoggingExcInfo
|
4988
|
+
info_tuple: LoggingExcInfoTuple
|
4989
|
+
|
4990
|
+
@classmethod
|
4991
|
+
def build(
|
4992
|
+
cls,
|
4993
|
+
arg: LoggingExcInfoArg = False,
|
4994
|
+
) -> ta.Optional['LoggingContextInfos.Exc']:
|
4995
|
+
if arg is True:
|
4996
|
+
sys_exc_info = sys.exc_info()
|
4997
|
+
if sys_exc_info[0] is not None:
|
4998
|
+
arg = sys_exc_info
|
4999
|
+
else:
|
5000
|
+
arg = None
|
5001
|
+
elif arg is False:
|
5002
|
+
arg = None
|
5003
|
+
if arg is None:
|
5004
|
+
return None
|
5005
|
+
|
5006
|
+
info: LoggingExcInfo = arg
|
5007
|
+
if isinstance(info, BaseException):
|
5008
|
+
info_tuple: LoggingExcInfoTuple = (type(info), info, info.__traceback__) # noqa
|
5009
|
+
else:
|
5010
|
+
info_tuple = info
|
5011
|
+
|
5012
|
+
return cls(
|
5013
|
+
info=info,
|
5014
|
+
info_tuple=info_tuple,
|
5015
|
+
)
|
5016
|
+
|
5017
|
+
@logging_context_info
|
5018
|
+
@ta.final
|
5019
|
+
class Caller(ta.NamedTuple):
|
5020
|
+
file_path: str
|
5021
|
+
line_no: int
|
5022
|
+
func_name: str
|
5023
|
+
stack_info: ta.Optional[str]
|
5024
|
+
|
5025
|
+
@classmethod
|
5026
|
+
def is_internal_frame(cls, frame: types.FrameType) -> bool:
|
5027
|
+
file_path = os.path.normcase(frame.f_code.co_filename)
|
5028
|
+
|
5029
|
+
# Yes, really.
|
5030
|
+
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204 # noqa
|
5031
|
+
# https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
|
5032
|
+
if 'importlib' in file_path and '_bootstrap' in file_path:
|
5033
|
+
return True
|
5034
|
+
|
5035
|
+
return False
|
5036
|
+
|
5037
|
+
@classmethod
|
5038
|
+
def find_frame(cls, stack_offset: int = 0) -> ta.Optional[types.FrameType]:
|
5039
|
+
f: ta.Optional[types.FrameType] = sys._getframe(2 + stack_offset) # noqa
|
5040
|
+
|
5041
|
+
while f is not None:
|
5042
|
+
# NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful,
|
5043
|
+
# manual stack_offset management.
|
5044
|
+
if hasattr(f, 'f_code'):
|
5045
|
+
return f
|
5046
|
+
|
5047
|
+
f = f.f_back
|
4949
5048
|
|
4950
|
-
@classmethod
|
4951
|
-
def find(
|
4952
|
-
cls,
|
4953
|
-
ofs: int = 0,
|
4954
|
-
*,
|
4955
|
-
stack_info: bool = False,
|
4956
|
-
) -> ta.Optional['LoggingCaller']:
|
4957
|
-
if (f := cls.find_frame(ofs + 1)) is None:
|
4958
5049
|
return None
|
4959
5050
|
|
4960
|
-
|
4961
|
-
|
4962
|
-
|
4963
|
-
|
4964
|
-
|
4965
|
-
|
4966
|
-
|
4967
|
-
if
|
4968
|
-
|
5051
|
+
@classmethod
|
5052
|
+
def build(
|
5053
|
+
cls,
|
5054
|
+
stack_offset: int = 0,
|
5055
|
+
*,
|
5056
|
+
stack_info: bool = False,
|
5057
|
+
) -> ta.Optional['LoggingContextInfos.Caller']:
|
5058
|
+
if (f := cls.find_frame(stack_offset + 1)) is None:
|
5059
|
+
return None
|
4969
5060
|
|
4970
|
-
|
4971
|
-
|
4972
|
-
|
4973
|
-
|
4974
|
-
|
5061
|
+
# https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
|
5062
|
+
sinfo = None
|
5063
|
+
if stack_info:
|
5064
|
+
sio = io.StringIO()
|
5065
|
+
traceback.print_stack(f, file=sio)
|
5066
|
+
sinfo = sio.getvalue()
|
5067
|
+
sio.close()
|
5068
|
+
if sinfo[-1] == '\n':
|
5069
|
+
sinfo = sinfo[:-1]
|
5070
|
+
|
5071
|
+
return cls(
|
5072
|
+
file_path=f.f_code.co_filename,
|
5073
|
+
line_no=f.f_lineno or 0,
|
5074
|
+
func_name=f.f_code.co_name,
|
5075
|
+
stack_info=sinfo,
|
5076
|
+
)
|
5077
|
+
|
5078
|
+
@logging_context_info
|
5079
|
+
@ta.final
|
5080
|
+
class SourceFile(ta.NamedTuple):
|
5081
|
+
file_name: str
|
5082
|
+
module: str
|
5083
|
+
|
5084
|
+
@classmethod
|
5085
|
+
def build(cls, caller_file_path: ta.Optional[str]) -> ta.Optional['LoggingContextInfos.SourceFile']:
|
5086
|
+
if caller_file_path is None:
|
5087
|
+
return None
|
5088
|
+
|
5089
|
+
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
|
5090
|
+
try:
|
5091
|
+
file_name = os.path.basename(caller_file_path)
|
5092
|
+
module = os.path.splitext(file_name)[0]
|
5093
|
+
except (TypeError, ValueError, AttributeError):
|
5094
|
+
return None
|
5095
|
+
|
5096
|
+
return cls(
|
5097
|
+
file_name=file_name,
|
5098
|
+
module=module,
|
5099
|
+
)
|
5100
|
+
|
5101
|
+
@logging_context_info
|
5102
|
+
@ta.final
|
5103
|
+
class Thread(ta.NamedTuple):
|
5104
|
+
ident: int
|
5105
|
+
native_id: ta.Optional[int]
|
5106
|
+
name: str
|
5107
|
+
|
5108
|
+
@classmethod
|
5109
|
+
def build(cls) -> 'LoggingContextInfos.Thread':
|
5110
|
+
return cls(
|
5111
|
+
ident=threading.get_ident(),
|
5112
|
+
native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
|
5113
|
+
name=threading.current_thread().name,
|
5114
|
+
)
|
5115
|
+
|
5116
|
+
@logging_context_info
|
5117
|
+
@ta.final
|
5118
|
+
class Process(ta.NamedTuple):
|
5119
|
+
pid: int
|
5120
|
+
|
5121
|
+
@classmethod
|
5122
|
+
def build(cls) -> 'LoggingContextInfos.Process':
|
5123
|
+
return cls(
|
5124
|
+
pid=os.getpid(),
|
5125
|
+
)
|
5126
|
+
|
5127
|
+
@logging_context_info
|
5128
|
+
@ta.final
|
5129
|
+
class Multiprocessing(ta.NamedTuple):
|
5130
|
+
process_name: str
|
5131
|
+
|
5132
|
+
@classmethod
|
5133
|
+
def build(cls) -> ta.Optional['LoggingContextInfos.Multiprocessing']:
|
5134
|
+
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
|
5135
|
+
if (mp := sys.modules.get('multiprocessing')) is None:
|
5136
|
+
return None
|
5137
|
+
|
5138
|
+
return cls(
|
5139
|
+
process_name=mp.current_process().name,
|
5140
|
+
)
|
5141
|
+
|
5142
|
+
@logging_context_info
|
5143
|
+
@ta.final
|
5144
|
+
class AsyncioTask(ta.NamedTuple):
|
5145
|
+
name: str
|
5146
|
+
|
5147
|
+
@classmethod
|
5148
|
+
def build(cls) -> ta.Optional['LoggingContextInfos.AsyncioTask']:
|
5149
|
+
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
|
5150
|
+
if (asyncio := sys.modules.get('asyncio')) is None:
|
5151
|
+
return None
|
5152
|
+
|
5153
|
+
try:
|
5154
|
+
task = asyncio.current_task()
|
5155
|
+
except Exception: # noqa
|
5156
|
+
return None
|
5157
|
+
|
5158
|
+
if task is None:
|
5159
|
+
return None
|
5160
|
+
|
5161
|
+
return cls(
|
5162
|
+
name=task.get_name(), # Always non-None
|
5163
|
+
)
|
5164
|
+
|
5165
|
+
|
5166
|
+
##
|
5167
|
+
|
5168
|
+
|
5169
|
+
class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
|
5170
|
+
pass
|
5171
|
+
|
5172
|
+
|
5173
|
+
def _check_logging_start_time() -> None:
|
5174
|
+
if (x := LoggingContextInfos.Time.get_std_start_ns()) < (t := time.time()):
|
5175
|
+
import warnings # noqa
|
5176
|
+
|
5177
|
+
warnings.warn(
|
5178
|
+
f'Unexpected logging start time detected: '
|
5179
|
+
f'get_std_start_ns={x}, '
|
5180
|
+
f'time.time()={t}',
|
5181
|
+
UnexpectedLoggingStartTimeWarning,
|
4975
5182
|
)
|
4976
5183
|
|
4977
5184
|
|
5185
|
+
_check_logging_start_time()
|
5186
|
+
|
5187
|
+
|
4978
5188
|
########################################
|
4979
5189
|
# ../../../../../omlish/logs/std/json.py
|
4980
5190
|
"""
|
@@ -5032,91 +5242,6 @@ class JsonLoggingFormatter(logging.Formatter):
|
|
5032
5242
|
return self._json_dumps(dct)
|
5033
5243
|
|
5034
5244
|
|
5035
|
-
########################################
|
5036
|
-
# ../../../../../omlish/logs/times.py
|
5037
|
-
|
5038
|
-
|
5039
|
-
##
|
5040
|
-
|
5041
|
-
|
5042
|
-
@logging_context_info
|
5043
|
-
@ta.final
|
5044
|
-
class LoggingTimeFields(ta.NamedTuple):
|
5045
|
-
"""Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
|
5046
|
-
|
5047
|
-
created: float
|
5048
|
-
msecs: float
|
5049
|
-
relative_created: float
|
5050
|
-
|
5051
|
-
@classmethod
|
5052
|
-
def get_std_start_time_ns(cls) -> int:
|
5053
|
-
x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
|
5054
|
-
|
5055
|
-
# Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
|
5056
|
-
# int.
|
5057
|
-
#
|
5058
|
-
# See:
|
5059
|
-
# - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
5060
|
-
#
|
5061
|
-
if isinstance(x, float):
|
5062
|
-
return int(x * 1e9)
|
5063
|
-
else:
|
5064
|
-
return x
|
5065
|
-
|
5066
|
-
@classmethod
|
5067
|
-
def build(
|
5068
|
-
cls,
|
5069
|
-
time_ns: int,
|
5070
|
-
*,
|
5071
|
-
start_time_ns: ta.Optional[int] = None,
|
5072
|
-
) -> 'LoggingTimeFields':
|
5073
|
-
# https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
5074
|
-
created = time_ns / 1e9 # ns to float seconds
|
5075
|
-
|
5076
|
-
# Get the number of whole milliseconds (0-999) in the fractional part of seconds.
|
5077
|
-
# Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
|
5078
|
-
# Convert to float by adding 0.0 for historical reasons. See gh-89047
|
5079
|
-
msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
|
5080
|
-
|
5081
|
-
# https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
|
5082
|
-
if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
|
5083
|
-
# ns -> sec conversion can round up, e.g:
|
5084
|
-
# 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
|
5085
|
-
msecs = 0.0
|
5086
|
-
|
5087
|
-
if start_time_ns is None:
|
5088
|
-
start_time_ns = cls.get_std_start_time_ns()
|
5089
|
-
relative_created = (time_ns - start_time_ns) / 1e6
|
5090
|
-
|
5091
|
-
return cls(
|
5092
|
-
created,
|
5093
|
-
msecs,
|
5094
|
-
relative_created,
|
5095
|
-
)
|
5096
|
-
|
5097
|
-
|
5098
|
-
##
|
5099
|
-
|
5100
|
-
|
5101
|
-
class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
|
5102
|
-
pass
|
5103
|
-
|
5104
|
-
|
5105
|
-
def _check_logging_start_time() -> None:
|
5106
|
-
if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
|
5107
|
-
import warnings # noqa
|
5108
|
-
|
5109
|
-
warnings.warn(
|
5110
|
-
f'Unexpected logging start time detected: '
|
5111
|
-
f'get_std_start_time_ns={x}, '
|
5112
|
-
f'time.time()={t}',
|
5113
|
-
UnexpectedLoggingStartTimeWarning,
|
5114
|
-
)
|
5115
|
-
|
5116
|
-
|
5117
|
-
_check_logging_start_time()
|
5118
|
-
|
5119
|
-
|
5120
5245
|
########################################
|
5121
5246
|
# ../../logs.py
|
5122
5247
|
"""
|
@@ -5328,68 +5453,36 @@ def load_config_file_obj(
|
|
5328
5453
|
|
5329
5454
|
|
5330
5455
|
class LoggingContext(Abstract):
|
5331
|
-
@property
|
5332
|
-
@abc.abstractmethod
|
5333
|
-
def level(self) -> NamedLogLevel:
|
5334
|
-
raise NotImplementedError
|
5335
|
-
|
5336
|
-
#
|
5337
|
-
|
5338
|
-
@property
|
5339
|
-
@abc.abstractmethod
|
5340
|
-
def time_ns(self) -> int:
|
5341
|
-
raise NotImplementedError
|
5342
|
-
|
5343
|
-
@property
|
5344
5456
|
@abc.abstractmethod
|
5345
|
-
def
|
5457
|
+
def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
|
5346
5458
|
raise NotImplementedError
|
5347
5459
|
|
5348
|
-
|
5460
|
+
@ta.final
|
5461
|
+
def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
|
5462
|
+
return self.get_info(ty)
|
5349
5463
|
|
5350
|
-
@
|
5351
|
-
|
5352
|
-
|
5353
|
-
|
5464
|
+
@ta.final
|
5465
|
+
def must_get_info(self, ty: ta.Type[LoggingContextInfoT]) -> LoggingContextInfoT:
|
5466
|
+
if (info := self.get_info(ty)) is None:
|
5467
|
+
raise TypeError(f'LoggingContextInfo absent: {ty}')
|
5468
|
+
return info
|
5354
5469
|
|
5355
|
-
|
5356
|
-
@abc.abstractmethod
|
5357
|
-
def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
|
5358
|
-
raise NotImplementedError
|
5470
|
+
##
|
5359
5471
|
|
5360
|
-
#
|
5361
5472
|
|
5473
|
+
class CaptureLoggingContext(LoggingContext, Abstract):
|
5362
5474
|
@abc.abstractmethod
|
5363
|
-
def
|
5364
|
-
|
5475
|
+
def set_basic(
|
5476
|
+
self,
|
5477
|
+
name: str,
|
5365
5478
|
|
5366
|
-
|
5367
|
-
|
5479
|
+
msg: ta.Union[str, tuple, LoggingMsgFn],
|
5480
|
+
args: tuple,
|
5481
|
+
) -> 'CaptureLoggingContext':
|
5368
5482
|
raise NotImplementedError
|
5369
5483
|
|
5370
5484
|
#
|
5371
5485
|
|
5372
|
-
@abc.abstractmethod
|
5373
|
-
def thread(self) -> ta.Optional[LoggingThreadInfo]:
|
5374
|
-
raise NotImplementedError
|
5375
|
-
|
5376
|
-
@abc.abstractmethod
|
5377
|
-
def process(self) -> ta.Optional[LoggingProcessInfo]:
|
5378
|
-
raise NotImplementedError
|
5379
|
-
|
5380
|
-
@abc.abstractmethod
|
5381
|
-
def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
|
5382
|
-
raise NotImplementedError
|
5383
|
-
|
5384
|
-
@abc.abstractmethod
|
5385
|
-
def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
|
5386
|
-
raise NotImplementedError
|
5387
|
-
|
5388
|
-
|
5389
|
-
##
|
5390
|
-
|
5391
|
-
|
5392
|
-
class CaptureLoggingContext(LoggingContext, Abstract):
|
5393
5486
|
class AlreadyCapturedError(Exception):
|
5394
5487
|
pass
|
5395
5488
|
|
@@ -5420,80 +5513,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
|
|
5420
5513
|
|
5421
5514
|
exc_info: LoggingExcInfoArg = False,
|
5422
5515
|
|
5423
|
-
caller: ta.Union[
|
5516
|
+
caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
|
5424
5517
|
stack_offset: int = 0,
|
5425
5518
|
stack_info: bool = False,
|
5426
5519
|
) -> None:
|
5427
|
-
|
5428
|
-
|
5429
|
-
#
|
5520
|
+
# TODO: Name, Msg, Extra
|
5430
5521
|
|
5431
5522
|
if time_ns is None:
|
5432
5523
|
time_ns = time.time_ns()
|
5433
|
-
self._time_ns: int = time_ns
|
5434
|
-
|
5435
|
-
#
|
5436
|
-
|
5437
|
-
if exc_info is True:
|
5438
|
-
sys_exc_info = sys.exc_info()
|
5439
|
-
if sys_exc_info[0] is not None:
|
5440
|
-
exc_info = sys_exc_info
|
5441
|
-
else:
|
5442
|
-
exc_info = None
|
5443
|
-
elif exc_info is False:
|
5444
|
-
exc_info = None
|
5445
|
-
|
5446
|
-
if exc_info is not None:
|
5447
|
-
self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
|
5448
|
-
if isinstance(exc_info, BaseException):
|
5449
|
-
self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
|
5450
|
-
else:
|
5451
|
-
self._exc_info_tuple = exc_info
|
5452
5524
|
|
5453
|
-
|
5525
|
+
self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {}
|
5526
|
+
self._set_info(
|
5527
|
+
LoggingContextInfos.Level.build(level),
|
5528
|
+
LoggingContextInfos.Time.build(time_ns),
|
5529
|
+
LoggingContextInfos.Exc.build(exc_info),
|
5530
|
+
)
|
5454
5531
|
|
5455
5532
|
if caller is not CaptureLoggingContextImpl.NOT_SET:
|
5456
|
-
self.
|
5533
|
+
self._infos[LoggingContextInfos.Caller] = caller
|
5457
5534
|
else:
|
5458
5535
|
self._stack_offset = stack_offset
|
5459
5536
|
self._stack_info = stack_info
|
5460
5537
|
|
5461
|
-
|
5462
|
-
|
5463
|
-
|
5464
|
-
|
5465
|
-
return self
|
5466
|
-
|
5467
|
-
#
|
5468
|
-
|
5469
|
-
@property
|
5470
|
-
def time_ns(self) -> int:
|
5471
|
-
return self._time_ns
|
5472
|
-
|
5473
|
-
_times: LoggingTimeFields
|
5474
|
-
|
5475
|
-
@property
|
5476
|
-
def times(self) -> LoggingTimeFields:
|
5477
|
-
try:
|
5478
|
-
return self._times
|
5479
|
-
except AttributeError:
|
5480
|
-
pass
|
5538
|
+
def _set_info(self, *infos: ta.Optional[LoggingContextInfo]) -> 'CaptureLoggingContextImpl':
|
5539
|
+
for info in infos:
|
5540
|
+
if info is not None:
|
5541
|
+
self._infos[type(info)] = info
|
5542
|
+
return self
|
5481
5543
|
|
5482
|
-
|
5483
|
-
return
|
5544
|
+
def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
|
5545
|
+
return self._infos.get(ty)
|
5484
5546
|
|
5485
|
-
|
5486
|
-
|
5487
|
-
_exc_info: ta.Optional[LoggingExcInfo] = None
|
5488
|
-
_exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
|
5547
|
+
##
|
5489
5548
|
|
5490
|
-
|
5491
|
-
|
5492
|
-
|
5549
|
+
def set_basic(
|
5550
|
+
self,
|
5551
|
+
name: str,
|
5493
5552
|
|
5494
|
-
|
5495
|
-
|
5496
|
-
|
5553
|
+
msg: ta.Union[str, tuple, LoggingMsgFn],
|
5554
|
+
args: tuple,
|
5555
|
+
) -> 'CaptureLoggingContextImpl':
|
5556
|
+
return self._set_info(
|
5557
|
+
LoggingContextInfos.Name(name),
|
5558
|
+
LoggingContextInfos.Msg.build(msg, *args),
|
5559
|
+
)
|
5497
5560
|
|
5498
5561
|
##
|
5499
5562
|
|
@@ -5507,74 +5570,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
|
|
5507
5570
|
|
5508
5571
|
_has_captured: bool = False
|
5509
5572
|
|
5510
|
-
_caller: ta.Optional[LoggingCaller]
|
5511
|
-
_source_file: ta.Optional[LoggingSourceFileInfo]
|
5512
|
-
|
5513
|
-
_thread: ta.Optional[LoggingThreadInfo]
|
5514
|
-
_process: ta.Optional[LoggingProcessInfo]
|
5515
|
-
_multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
|
5516
|
-
_asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
|
5517
|
-
|
5518
5573
|
def capture(self) -> None:
|
5519
5574
|
if self._has_captured:
|
5520
5575
|
raise CaptureLoggingContextImpl.AlreadyCapturedError
|
5521
5576
|
self._has_captured = True
|
5522
5577
|
|
5523
|
-
if not
|
5524
|
-
self.
|
5578
|
+
if LoggingContextInfos.Caller not in self._infos:
|
5579
|
+
self._set_info(LoggingContextInfos.Caller.build(
|
5525
5580
|
self._stack_offset + 1,
|
5526
5581
|
stack_info=self._stack_info,
|
5527
|
-
)
|
5528
|
-
|
5529
|
-
if (caller := self._caller) is not None:
|
5530
|
-
self._source_file = LoggingSourceFileInfo.build(caller.file_path)
|
5531
|
-
else:
|
5532
|
-
self._source_file = None
|
5533
|
-
|
5534
|
-
self._thread = LoggingThreadInfo.build()
|
5535
|
-
self._process = LoggingProcessInfo.build()
|
5536
|
-
self._multiprocessing = LoggingMultiprocessingInfo.build()
|
5537
|
-
self._asyncio_task = LoggingAsyncioTaskInfo.build()
|
5538
|
-
|
5539
|
-
#
|
5540
|
-
|
5541
|
-
def caller(self) -> ta.Optional[LoggingCaller]:
|
5542
|
-
try:
|
5543
|
-
return self._caller
|
5544
|
-
except AttributeError:
|
5545
|
-
raise CaptureLoggingContext.NotCapturedError from None
|
5546
|
-
|
5547
|
-
def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
|
5548
|
-
try:
|
5549
|
-
return self._source_file
|
5550
|
-
except AttributeError:
|
5551
|
-
raise CaptureLoggingContext.NotCapturedError from None
|
5552
|
-
|
5553
|
-
#
|
5554
|
-
|
5555
|
-
def thread(self) -> ta.Optional[LoggingThreadInfo]:
|
5556
|
-
try:
|
5557
|
-
return self._thread
|
5558
|
-
except AttributeError:
|
5559
|
-
raise CaptureLoggingContext.NotCapturedError from None
|
5560
|
-
|
5561
|
-
def process(self) -> ta.Optional[LoggingProcessInfo]:
|
5562
|
-
try:
|
5563
|
-
return self._process
|
5564
|
-
except AttributeError:
|
5565
|
-
raise CaptureLoggingContext.NotCapturedError from None
|
5582
|
+
))
|
5566
5583
|
|
5567
|
-
|
5568
|
-
|
5569
|
-
|
5570
|
-
|
5571
|
-
raise CaptureLoggingContext.NotCapturedError from None
|
5584
|
+
if (caller := self[LoggingContextInfos.Caller]) is not None:
|
5585
|
+
self._set_info(LoggingContextInfos.SourceFile.build(
|
5586
|
+
caller.file_path,
|
5587
|
+
))
|
5572
5588
|
|
5573
|
-
|
5574
|
-
|
5575
|
-
|
5576
|
-
|
5577
|
-
|
5589
|
+
self._set_info(
|
5590
|
+
LoggingContextInfos.Thread.build(),
|
5591
|
+
LoggingContextInfos.Process.build(),
|
5592
|
+
LoggingContextInfos.Multiprocessing.build(),
|
5593
|
+
LoggingContextInfos.AsyncioTask.build(),
|
5594
|
+
)
|
5578
5595
|
|
5579
5596
|
|
5580
5597
|
########################################
|
@@ -5734,7 +5751,6 @@ def subprocess_maybe_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
|
|
5734
5751
|
|
5735
5752
|
|
5736
5753
|
class AnyLogger(Abstract, ta.Generic[T]):
|
5737
|
-
@ta.final
|
5738
5754
|
def is_enabled_for(self, level: LogLevel) -> bool:
|
5739
5755
|
return level >= self.get_effective_level()
|
5740
5756
|
|
@@ -5880,36 +5896,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
|
|
5880
5896
|
|
5881
5897
|
##
|
5882
5898
|
|
5883
|
-
@classmethod
|
5884
|
-
def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
|
5885
|
-
if callable(msg):
|
5886
|
-
if args:
|
5887
|
-
raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
|
5888
|
-
x = msg()
|
5889
|
-
if isinstance(x, str):
|
5890
|
-
return x, ()
|
5891
|
-
elif isinstance(x, tuple):
|
5892
|
-
if x:
|
5893
|
-
return x[0], x[1:]
|
5894
|
-
else:
|
5895
|
-
return '', ()
|
5896
|
-
else:
|
5897
|
-
raise TypeError(x)
|
5898
|
-
|
5899
|
-
elif isinstance(msg, tuple):
|
5900
|
-
if args:
|
5901
|
-
raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
|
5902
|
-
if msg:
|
5903
|
-
return msg[0], msg[1:]
|
5904
|
-
else:
|
5905
|
-
return '', ()
|
5906
|
-
|
5907
|
-
elif isinstance(msg, str):
|
5908
|
-
return msg, args
|
5909
|
-
|
5910
|
-
else:
|
5911
|
-
raise TypeError(msg)
|
5912
|
-
|
5913
5899
|
@abc.abstractmethod
|
5914
5900
|
def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
|
5915
5901
|
raise NotImplementedError
|
@@ -5933,7 +5919,7 @@ class AsyncLogger(AnyLogger[ta.Awaitable[None]], Abstract):
|
|
5933
5919
|
class AnyNopLogger(AnyLogger[T], Abstract):
|
5934
5920
|
@ta.final
|
5935
5921
|
def get_effective_level(self) -> LogLevel:
|
5936
|
-
return 999
|
5922
|
+
return -999
|
5937
5923
|
|
5938
5924
|
|
5939
5925
|
@ta.final
|
@@ -5950,137 +5936,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
|
|
5950
5936
|
|
5951
5937
|
########################################
|
5952
5938
|
# ../../../../../omlish/logs/std/records.py
|
5939
|
+
"""
|
5940
|
+
TODO:
|
5941
|
+
- TypedDict?
|
5942
|
+
"""
|
5953
5943
|
|
5954
5944
|
|
5955
5945
|
##
|
5956
5946
|
|
5957
5947
|
|
5958
|
-
|
5959
|
-
#
|
5960
|
-
#
|
5961
|
-
#
|
5962
|
-
#
|
5963
|
-
# - https://github.com/python/cpython/blob/
|
5964
|
-
#
|
5965
|
-
#
|
5966
|
-
# - name: str
|
5967
|
-
# - level: int
|
5968
|
-
# - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
|
5969
|
-
# - lineno: int - May be 0.
|
5970
|
-
# - msg: str
|
5971
|
-
# - args: tuple | dict | 1-tuple[dict]
|
5972
|
-
# - exc_info: LoggingExcInfoTuple | None
|
5973
|
-
# - func: str | None = None -> funcName
|
5974
|
-
# - sinfo: str | None = None -> stack_info
|
5975
|
-
#
|
5976
|
-
KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
|
5977
|
-
# Name of the logger used to log the call. Unmodified by ctor.
|
5978
|
-
name=str,
|
5948
|
+
class LoggingContextInfoRecordAdapters:
|
5949
|
+
# Ref:
|
5950
|
+
# - https://docs.python.org/3/library/logging.html#logrecord-attributes
|
5951
|
+
#
|
5952
|
+
# LogRecord:
|
5953
|
+
# - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
|
5954
|
+
# - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
|
5955
|
+
#
|
5979
5956
|
|
5980
|
-
|
5981
|
-
|
5982
|
-
msg=str,
|
5957
|
+
def __new__(cls, *args, **kwargs): # noqa
|
5958
|
+
raise TypeError
|
5983
5959
|
|
5984
|
-
|
5985
|
-
|
5986
|
-
|
5987
|
-
|
5960
|
+
class Adapter(Abstract, ta.Generic[T]):
|
5961
|
+
@property
|
5962
|
+
@abc.abstractmethod
|
5963
|
+
def info_cls(self) -> ta.Type[LoggingContextInfo]:
|
5964
|
+
raise NotImplementedError
|
5988
5965
|
|
5989
|
-
|
5966
|
+
#
|
5990
5967
|
|
5991
|
-
|
5992
|
-
|
5993
|
-
|
5968
|
+
@ta.final
|
5969
|
+
class NOT_SET: # noqa
|
5970
|
+
def __new__(cls, *args, **kwargs): # noqa
|
5971
|
+
raise TypeError
|
5994
5972
|
|
5995
|
-
|
5996
|
-
|
5973
|
+
class RecordAttr(ta.NamedTuple):
|
5974
|
+
name: str
|
5975
|
+
type: ta.Any
|
5976
|
+
default: ta.Any
|
5997
5977
|
|
5998
|
-
|
5978
|
+
# @abc.abstractmethod
|
5979
|
+
record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
|
5999
5980
|
|
6000
|
-
|
6001
|
-
|
6002
|
-
|
5981
|
+
@property
|
5982
|
+
@abc.abstractmethod
|
5983
|
+
def _record_attrs(self) -> ta.Union[
|
5984
|
+
ta.Mapping[str, ta.Any],
|
5985
|
+
ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
|
5986
|
+
]:
|
5987
|
+
raise NotImplementedError
|
6003
5988
|
|
6004
|
-
|
6005
|
-
filename=str,
|
5989
|
+
#
|
6006
5990
|
|
6007
|
-
|
6008
|
-
|
6009
|
-
|
5991
|
+
@abc.abstractmethod
|
5992
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
5993
|
+
raise NotImplementedError
|
6010
5994
|
|
6011
|
-
|
5995
|
+
#
|
6012
5996
|
|
6013
|
-
|
6014
|
-
|
5997
|
+
@abc.abstractmethod
|
5998
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
|
5999
|
+
raise NotImplementedError
|
6015
6000
|
|
6016
|
-
|
6017
|
-
exc_text=ta.Optional[str],
|
6001
|
+
#
|
6018
6002
|
|
6019
|
-
|
6003
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
6004
|
+
super().__init_subclass__(**kwargs)
|
6020
6005
|
|
6021
|
-
|
6022
|
-
|
6023
|
-
# unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
|
6024
|
-
# the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
|
6025
|
-
stack_info=ta.Optional[str],
|
6006
|
+
if Abstract in cls.__bases__:
|
6007
|
+
return
|
6026
6008
|
|
6027
|
-
|
6028
|
-
|
6029
|
-
|
6009
|
+
if 'record_attrs' in cls.__dict__:
|
6010
|
+
raise TypeError(cls)
|
6011
|
+
if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
|
6012
|
+
raise TypeError(ra)
|
6013
|
+
|
6014
|
+
rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
|
6015
|
+
for n, v in ra.items():
|
6016
|
+
if not n or not isinstance(n, str) or n in rd:
|
6017
|
+
raise AttributeError(n)
|
6018
|
+
if isinstance(v, tuple):
|
6019
|
+
t, d = v
|
6020
|
+
else:
|
6021
|
+
t, d = v, cls.NOT_SET
|
6022
|
+
rd[n] = cls.RecordAttr(
|
6023
|
+
name=n,
|
6024
|
+
type=t,
|
6025
|
+
default=d,
|
6026
|
+
)
|
6027
|
+
cls.record_attrs = rd
|
6030
6028
|
|
6031
|
-
|
6032
|
-
|
6033
|
-
|
6029
|
+
class RequiredAdapter(Adapter[T], Abstract):
|
6030
|
+
@property
|
6031
|
+
@abc.abstractmethod
|
6032
|
+
def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
|
6033
|
+
raise NotImplementedError
|
6034
6034
|
|
6035
|
-
|
6035
|
+
#
|
6036
6036
|
|
6037
|
-
|
6038
|
-
|
6039
|
-
|
6040
|
-
|
6041
|
-
|
6042
|
-
|
6043
|
-
created=float,
|
6037
|
+
@ta.final
|
6038
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
6039
|
+
if (info := ctx.get_info(self.info_cls)) is not None:
|
6040
|
+
return self._info_to_record(info)
|
6041
|
+
else:
|
6042
|
+
raise TypeError # FIXME: fallback?
|
6044
6043
|
|
6045
|
-
|
6046
|
-
|
6044
|
+
@abc.abstractmethod
|
6045
|
+
def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
|
6046
|
+
raise NotImplementedError
|
6047
6047
|
|
6048
|
-
|
6049
|
-
relativeCreated=float,
|
6048
|
+
#
|
6050
6049
|
|
6051
|
-
|
6050
|
+
@abc.abstractmethod
|
6051
|
+
def record_to_info(self, rec: logging.LogRecord) -> T:
|
6052
|
+
raise NotImplementedError
|
6052
6053
|
|
6053
|
-
|
6054
|
-
thread=ta.Optional[int],
|
6054
|
+
#
|
6055
6055
|
|
6056
|
-
|
6057
|
-
|
6056
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
6057
|
+
super().__init_subclass__(**kwargs)
|
6058
6058
|
|
6059
|
-
|
6059
|
+
if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
|
6060
|
+
raise TypeError(cls.record_attrs)
|
6061
|
+
|
6062
|
+
class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
|
6063
|
+
@property
|
6064
|
+
@abc.abstractmethod
|
6065
|
+
def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
|
6066
|
+
raise NotImplementedError
|
6067
|
+
|
6068
|
+
record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
|
6069
|
+
|
6070
|
+
#
|
6071
|
+
|
6072
|
+
@ta.final
|
6073
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
6074
|
+
if (info := ctx.get_info(self.info_cls)) is not None:
|
6075
|
+
return self._info_to_record(info)
|
6076
|
+
else:
|
6077
|
+
return self.record_defaults
|
6078
|
+
|
6079
|
+
@abc.abstractmethod
|
6080
|
+
def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
|
6081
|
+
raise NotImplementedError
|
6082
|
+
|
6083
|
+
#
|
6084
|
+
|
6085
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
6086
|
+
super().__init_subclass__(**kwargs)
|
6087
|
+
|
6088
|
+
dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
|
6089
|
+
if any(d is cls.NOT_SET for d in dd.values()):
|
6090
|
+
raise TypeError(cls.record_attrs)
|
6091
|
+
cls.record_defaults = dd
|
6060
6092
|
|
6061
|
-
# Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
|
6062
|
-
# 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
|
6063
|
-
# as 'MainProcess'.
|
6064
6093
|
#
|
6065
|
-
|
6094
|
+
|
6095
|
+
class Name(RequiredAdapter[LoggingContextInfos.Name]):
|
6096
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
|
6097
|
+
|
6098
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
6099
|
+
# Name of the logger used to log the call. Unmodified by ctor.
|
6100
|
+
name=str,
|
6101
|
+
)
|
6102
|
+
|
6103
|
+
def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
|
6104
|
+
return dict(
|
6105
|
+
name=info.name,
|
6106
|
+
)
|
6107
|
+
|
6108
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
|
6109
|
+
return LoggingContextInfos.Name(
|
6110
|
+
name=rec.name,
|
6111
|
+
)
|
6112
|
+
|
6113
|
+
class Level(RequiredAdapter[LoggingContextInfos.Level]):
|
6114
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
|
6115
|
+
|
6116
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
6117
|
+
# Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
|
6118
|
+
# `getLevelName(level)`.
|
6119
|
+
levelname=str,
|
6120
|
+
|
6121
|
+
# Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
|
6122
|
+
levelno=int,
|
6123
|
+
)
|
6124
|
+
|
6125
|
+
def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
|
6126
|
+
return dict(
|
6127
|
+
levelname=info.name,
|
6128
|
+
levelno=int(info.level),
|
6129
|
+
)
|
6130
|
+
|
6131
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
|
6132
|
+
return LoggingContextInfos.Level.build(rec.levelno)
|
6133
|
+
|
6134
|
+
class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
|
6135
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
|
6136
|
+
|
6137
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
6138
|
+
# The format string passed in the original logging call. Merged with args to produce message, or an
|
6139
|
+
# arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
|
6140
|
+
msg=str,
|
6141
|
+
|
6142
|
+
# The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
|
6143
|
+
# (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
|
6144
|
+
# Mapping into just the mapping, but is otherwise unmodified.
|
6145
|
+
args=ta.Union[tuple, dict, None],
|
6146
|
+
)
|
6147
|
+
|
6148
|
+
def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
|
6149
|
+
return dict(
|
6150
|
+
msg=info.msg,
|
6151
|
+
args=info.args,
|
6152
|
+
)
|
6153
|
+
|
6154
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
|
6155
|
+
return LoggingContextInfos.Msg(
|
6156
|
+
msg=rec.msg,
|
6157
|
+
args=rec.args,
|
6158
|
+
)
|
6159
|
+
|
6160
|
+
# FIXME: handled specially - all unknown attrs on LogRecord
|
6161
|
+
# class Extra(Adapter[LoggingContextInfos.Extra]):
|
6162
|
+
# _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
|
6066
6163
|
#
|
6067
|
-
#
|
6068
|
-
#
|
6164
|
+
# def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
|
6165
|
+
# # FIXME:
|
6166
|
+
# # if extra is not None:
|
6167
|
+
# # for key in extra:
|
6168
|
+
# # if (key in ["message", "asctime"]) or (key in rv.__dict__):
|
6169
|
+
# # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
|
6170
|
+
# # rv.__dict__[key] = extra[key]
|
6171
|
+
# return dict()
|
6069
6172
|
#
|
6070
|
-
|
6173
|
+
# def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
|
6174
|
+
# return None
|
6175
|
+
|
6176
|
+
class Time(RequiredAdapter[LoggingContextInfos.Time]):
|
6177
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
|
6178
|
+
|
6179
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
6180
|
+
# Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
|
6181
|
+
# `time.time()`.
|
6182
|
+
#
|
6183
|
+
# See:
|
6184
|
+
# - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
6185
|
+
# - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
|
6186
|
+
#
|
6187
|
+
created=float,
|
6188
|
+
|
6189
|
+
# Millisecond portion of the time when the LogRecord was created.
|
6190
|
+
msecs=float,
|
6191
|
+
|
6192
|
+
# Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
|
6193
|
+
relativeCreated=float,
|
6194
|
+
)
|
6071
6195
|
|
6072
|
-
|
6073
|
-
|
6074
|
-
|
6196
|
+
def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
|
6197
|
+
return dict(
|
6198
|
+
created=info.secs,
|
6199
|
+
msecs=info.msecs,
|
6200
|
+
relativeCreated=info.relative_secs,
|
6201
|
+
)
|
6075
6202
|
|
6076
|
-
|
6203
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
|
6204
|
+
return LoggingContextInfos.Time.build(
|
6205
|
+
int(rec.created * 1e9),
|
6206
|
+
)
|
6077
6207
|
|
6078
|
-
|
6079
|
-
|
6080
|
-
|
6081
|
-
|
6208
|
+
class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
|
6209
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
|
6210
|
+
|
6211
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
6212
|
+
# Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
|
6213
|
+
exc_info=(ta.Optional[LoggingExcInfoTuple], None),
|
6214
|
+
|
6215
|
+
# Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
|
6216
|
+
exc_text=(ta.Optional[str], None),
|
6217
|
+
)
|
6218
|
+
|
6219
|
+
def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
|
6220
|
+
return dict(
|
6221
|
+
exc_info=info.info_tuple,
|
6222
|
+
exc_text=None,
|
6223
|
+
)
|
6224
|
+
|
6225
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
|
6226
|
+
# FIXME:
|
6227
|
+
# error: Argument 1 to "build" of "Exc" has incompatible type
|
6228
|
+
# "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
|
6229
|
+
# "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
|
6230
|
+
return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
|
6231
|
+
|
6232
|
+
class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
|
6233
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
|
6234
|
+
|
6235
|
+
_UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
|
6236
|
+
_UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
|
6237
|
+
|
6238
|
+
_STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
|
6239
|
+
|
6240
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
6241
|
+
# Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
|
6242
|
+
# default to "(unknown file)" by Logger.findCaller / Logger._log.
|
6243
|
+
pathname=(str, _UNKNOWN_PATH_NAME),
|
6244
|
+
|
6245
|
+
# Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
|
6246
|
+
# y Logger.findCaller / Logger._log.
|
6247
|
+
lineno=(int, 0),
|
6248
|
+
|
6249
|
+
# Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
|
6250
|
+
# "(unknown function)" by Logger.findCaller / Logger._log.
|
6251
|
+
funcName=(str, _UNKNOWN_FUNC_NAME),
|
6252
|
+
|
6253
|
+
# Stack frame information (where available) from the bottom of the stack in the current thread, up to and
|
6254
|
+
# including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
|
6255
|
+
# to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
|
6256
|
+
# `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
|
6257
|
+
# stripped of exactly one trailing `\n` if present.
|
6258
|
+
stack_info=(ta.Optional[str], None),
|
6259
|
+
)
|
6260
|
+
|
6261
|
+
def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
|
6262
|
+
if (sinfo := caller.stack_info) is not None:
|
6263
|
+
stack_info: ta.Optional[str] = '\n'.join([
|
6264
|
+
self._STACK_INFO_PREFIX,
|
6265
|
+
sinfo[1:] if sinfo.endswith('\n') else sinfo,
|
6266
|
+
])
|
6267
|
+
else:
|
6268
|
+
stack_info = None
|
6269
|
+
|
6270
|
+
return dict(
|
6271
|
+
pathname=caller.file_path,
|
6272
|
+
|
6273
|
+
lineno=caller.line_no,
|
6274
|
+
funcName=caller.func_name,
|
6275
|
+
|
6276
|
+
stack_info=stack_info,
|
6277
|
+
)
|
6278
|
+
|
6279
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
|
6280
|
+
# FIXME: piecemeal?
|
6281
|
+
# FIXME: strip _STACK_INFO_PREFIX
|
6282
|
+
raise NotImplementedError
|
6283
|
+
|
6284
|
+
class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
|
6285
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
|
6286
|
+
|
6287
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
6288
|
+
# Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
|
6289
|
+
# pathname.
|
6290
|
+
filename=str,
|
6291
|
+
|
6292
|
+
# Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
|
6293
|
+
# "Unknown module".
|
6294
|
+
module=str,
|
6295
|
+
)
|
6296
|
+
|
6297
|
+
_UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
|
6298
|
+
|
6299
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
6300
|
+
if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
|
6301
|
+
return dict(
|
6302
|
+
filename=info.file_name,
|
6303
|
+
module=info.module,
|
6304
|
+
)
|
6305
|
+
|
6306
|
+
if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
|
6307
|
+
return dict(
|
6308
|
+
filename=caller.file_path,
|
6309
|
+
module=self._UNKNOWN_MODULE,
|
6310
|
+
)
|
6311
|
+
|
6312
|
+
return dict(
|
6313
|
+
filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
|
6314
|
+
module=self._UNKNOWN_MODULE,
|
6315
|
+
)
|
6316
|
+
|
6317
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
|
6318
|
+
if not (
|
6319
|
+
rec.module is None or
|
6320
|
+
rec.module == self._UNKNOWN_MODULE
|
6321
|
+
):
|
6322
|
+
return LoggingContextInfos.SourceFile(
|
6323
|
+
file_name=rec.filename,
|
6324
|
+
module=rec.module, # FIXME: piecemeal?
|
6325
|
+
)
|
6326
|
+
|
6327
|
+
return None
|
6328
|
+
|
6329
|
+
class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
|
6330
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
|
6331
|
+
|
6332
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
6333
|
+
# Thread ID if available, and `logging.logThreads` is truthy.
|
6334
|
+
thread=(ta.Optional[int], None),
|
6335
|
+
|
6336
|
+
# Thread name if available, and `logging.logThreads` is truthy.
|
6337
|
+
threadName=(ta.Optional[str], None),
|
6338
|
+
)
|
6339
|
+
|
6340
|
+
def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
|
6341
|
+
if logging.logThreads:
|
6342
|
+
return dict(
|
6343
|
+
thread=info.ident,
|
6344
|
+
threadName=info.name,
|
6345
|
+
)
|
6346
|
+
|
6347
|
+
return self.record_defaults
|
6348
|
+
|
6349
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
|
6350
|
+
if (
|
6351
|
+
(ident := rec.thread) is not None and
|
6352
|
+
(name := rec.threadName) is not None
|
6353
|
+
):
|
6354
|
+
return LoggingContextInfos.Thread(
|
6355
|
+
ident=ident,
|
6356
|
+
native_id=None,
|
6357
|
+
name=name,
|
6358
|
+
)
|
6359
|
+
|
6360
|
+
return None
|
6082
6361
|
|
6083
|
-
|
6362
|
+
class Process(OptionalAdapter[LoggingContextInfos.Process]):
|
6363
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
|
6364
|
+
|
6365
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
6366
|
+
# Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
|
6367
|
+
# otherwise None.
|
6368
|
+
process=(ta.Optional[int], None),
|
6369
|
+
)
|
6370
|
+
|
6371
|
+
def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
|
6372
|
+
if logging.logProcesses:
|
6373
|
+
return dict(
|
6374
|
+
process=info.pid,
|
6375
|
+
)
|
6376
|
+
|
6377
|
+
return self.record_defaults
|
6378
|
+
|
6379
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
|
6380
|
+
if (
|
6381
|
+
(pid := rec.process) is not None
|
6382
|
+
):
|
6383
|
+
return LoggingContextInfos.Process(
|
6384
|
+
pid=pid,
|
6385
|
+
)
|
6386
|
+
|
6387
|
+
return None
|
6388
|
+
|
6389
|
+
class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
|
6390
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
|
6391
|
+
|
6392
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
6393
|
+
# Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
|
6394
|
+
# 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
|
6395
|
+
# remains as 'MainProcess'.
|
6396
|
+
#
|
6397
|
+
# As noted by stdlib:
|
6398
|
+
#
|
6399
|
+
# Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
|
6400
|
+
# third-party code to run when multiprocessing calls import. See issue 8200 for an example
|
6401
|
+
#
|
6402
|
+
processName=(ta.Optional[str], None),
|
6403
|
+
)
|
6404
|
+
|
6405
|
+
def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
|
6406
|
+
if logging.logMultiprocessing:
|
6407
|
+
return dict(
|
6408
|
+
processName=info.process_name,
|
6409
|
+
)
|
6410
|
+
|
6411
|
+
return self.record_defaults
|
6412
|
+
|
6413
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
|
6414
|
+
if (
|
6415
|
+
(process_name := rec.processName) is not None
|
6416
|
+
):
|
6417
|
+
return LoggingContextInfos.Multiprocessing(
|
6418
|
+
process_name=process_name,
|
6419
|
+
)
|
6420
|
+
|
6421
|
+
return None
|
6422
|
+
|
6423
|
+
class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
|
6424
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
|
6425
|
+
|
6426
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
|
6427
|
+
# Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
|
6428
|
+
# `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
|
6429
|
+
taskName=(ta.Optional[str], None),
|
6430
|
+
)
|
6431
|
+
|
6432
|
+
def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
|
6433
|
+
if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
|
6434
|
+
return dict(
|
6435
|
+
taskName=info.name,
|
6436
|
+
)
|
6437
|
+
|
6438
|
+
return self.record_defaults
|
6439
|
+
|
6440
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
|
6441
|
+
if (
|
6442
|
+
(name := getattr(rec, 'taskName', None)) is not None
|
6443
|
+
):
|
6444
|
+
return LoggingContextInfos.AsyncioTask(
|
6445
|
+
name=name,
|
6446
|
+
)
|
6447
|
+
|
6448
|
+
return None
|
6449
|
+
|
6450
|
+
|
6451
|
+
_LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
|
6452
|
+
LoggingContextInfoRecordAdapters.Name(),
|
6453
|
+
LoggingContextInfoRecordAdapters.Level(),
|
6454
|
+
LoggingContextInfoRecordAdapters.Msg(),
|
6455
|
+
LoggingContextInfoRecordAdapters.Time(),
|
6456
|
+
LoggingContextInfoRecordAdapters.Exc(),
|
6457
|
+
LoggingContextInfoRecordAdapters.Caller(),
|
6458
|
+
LoggingContextInfoRecordAdapters.SourceFile(),
|
6459
|
+
LoggingContextInfoRecordAdapters.Thread(),
|
6460
|
+
LoggingContextInfoRecordAdapters.Process(),
|
6461
|
+
LoggingContextInfoRecordAdapters.Multiprocessing(),
|
6462
|
+
LoggingContextInfoRecordAdapters.AsyncioTask(),
|
6463
|
+
]
|
6464
|
+
|
6465
|
+
_LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
|
6466
|
+
ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
|
6467
|
+
}
|
6468
|
+
|
6469
|
+
|
6470
|
+
##
|
6471
|
+
|
6472
|
+
|
6473
|
+
KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
|
6474
|
+
a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
|
6475
|
+
)
|
6084
6476
|
|
6085
6477
|
|
6086
6478
|
# Formatter:
|
@@ -6104,14 +6496,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
|
|
6104
6496
|
KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
|
6105
6497
|
|
6106
6498
|
|
6107
|
-
##
|
6108
|
-
|
6109
|
-
|
6110
6499
|
class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
|
6111
6500
|
pass
|
6112
6501
|
|
6113
6502
|
|
6114
6503
|
def _check_std_logging_record_attrs() -> None:
|
6504
|
+
if (
|
6505
|
+
len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
|
6506
|
+
len(KNOWN_STD_LOGGING_RECORD_ATTR_SET)
|
6507
|
+
):
|
6508
|
+
raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
|
6509
|
+
|
6115
6510
|
rec_dct = dict(logging.makeLogRecord({}).__dict__)
|
6116
6511
|
|
6117
6512
|
if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
|
@@ -6130,120 +6525,24 @@ _check_std_logging_record_attrs()
|
|
6130
6525
|
|
6131
6526
|
|
6132
6527
|
class LoggingContextLogRecord(logging.LogRecord):
|
6133
|
-
|
6134
|
-
|
6135
|
-
|
6136
|
-
|
6137
|
-
|
6138
|
-
|
6139
|
-
|
6140
|
-
|
6141
|
-
|
6142
|
-
|
6143
|
-
|
6144
|
-
|
6145
|
-
|
6146
|
-
|
6147
|
-
# msg,
|
6148
|
-
# args,
|
6149
|
-
# exc_info,
|
6150
|
-
# func=None,
|
6151
|
-
# sinfo=None,
|
6152
|
-
# **kwargs,
|
6153
|
-
*,
|
6154
|
-
name: str,
|
6155
|
-
msg: str,
|
6156
|
-
args: ta.Union[tuple, dict],
|
6157
|
-
|
6158
|
-
_logging_context: LoggingContext,
|
6159
|
-
) -> None:
|
6160
|
-
ctx = _logging_context
|
6161
|
-
|
6162
|
-
self.name: str = name
|
6163
|
-
|
6164
|
-
self.msg: str = msg
|
6165
|
-
|
6166
|
-
# https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
|
6167
|
-
if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
|
6168
|
-
args = args[0] # type: ignore[assignment]
|
6169
|
-
self.args: ta.Union[tuple, dict] = args
|
6170
|
-
|
6171
|
-
self.levelname: str = logging.getLevelName(ctx.level)
|
6172
|
-
self.levelno: int = ctx.level
|
6173
|
-
|
6174
|
-
if (caller := ctx.caller()) is not None:
|
6175
|
-
self.pathname: str = caller.file_path
|
6176
|
-
else:
|
6177
|
-
self.pathname = self._UNKNOWN_PATH_NAME
|
6178
|
-
|
6179
|
-
if (src_file := ctx.source_file()) is not None:
|
6180
|
-
self.filename: str = src_file.file_name
|
6181
|
-
self.module: str = src_file.module
|
6182
|
-
else:
|
6183
|
-
self.filename = self.pathname
|
6184
|
-
self.module = self._UNKNOWN_MODULE
|
6185
|
-
|
6186
|
-
self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
|
6187
|
-
self.exc_text: ta.Optional[str] = None
|
6188
|
-
|
6189
|
-
# If ctx.build_caller() was never called, we simply don't have a stack trace.
|
6190
|
-
if caller is not None:
|
6191
|
-
if (sinfo := caller.stack_info) is not None:
|
6192
|
-
self.stack_info: ta.Optional[str] = '\n'.join([
|
6193
|
-
self._STACK_INFO_PREFIX,
|
6194
|
-
sinfo[1:] if sinfo.endswith('\n') else sinfo,
|
6195
|
-
])
|
6196
|
-
else:
|
6197
|
-
self.stack_info = None
|
6198
|
-
|
6199
|
-
self.lineno: int = caller.line_no
|
6200
|
-
self.funcName: str = caller.name
|
6201
|
-
|
6202
|
-
else:
|
6203
|
-
self.stack_info = None
|
6204
|
-
|
6205
|
-
self.lineno = 0
|
6206
|
-
self.funcName = self._UNKNOWN_FUNC_NAME
|
6207
|
-
|
6208
|
-
times = ctx.times
|
6209
|
-
self.created: float = times.created
|
6210
|
-
self.msecs: float = times.msecs
|
6211
|
-
self.relativeCreated: float = times.relative_created
|
6212
|
-
|
6213
|
-
if logging.logThreads:
|
6214
|
-
thread = check.not_none(ctx.thread())
|
6215
|
-
self.thread: ta.Optional[int] = thread.ident
|
6216
|
-
self.threadName: ta.Optional[str] = thread.name
|
6217
|
-
else:
|
6218
|
-
self.thread = None
|
6219
|
-
self.threadName = None
|
6220
|
-
|
6221
|
-
if logging.logProcesses:
|
6222
|
-
process = check.not_none(ctx.process())
|
6223
|
-
self.process: ta.Optional[int] = process.pid
|
6224
|
-
else:
|
6225
|
-
self.process = None
|
6226
|
-
|
6227
|
-
if logging.logMultiprocessing:
|
6228
|
-
if (mp := ctx.multiprocessing()) is not None:
|
6229
|
-
self.processName: ta.Optional[str] = mp.process_name
|
6230
|
-
else:
|
6231
|
-
self.processName = None
|
6232
|
-
else:
|
6233
|
-
self.processName = None
|
6234
|
-
|
6235
|
-
# Absent <3.12
|
6236
|
-
if getattr(logging, 'logAsyncioTasks', None):
|
6237
|
-
if (at := ctx.asyncio_task()) is not None:
|
6238
|
-
self.taskName: ta.Optional[str] = at.name
|
6239
|
-
else:
|
6240
|
-
self.taskName = None
|
6241
|
-
else:
|
6242
|
-
self.taskName = None
|
6528
|
+
# LogRecord.__init__ args:
|
6529
|
+
# - name: str
|
6530
|
+
# - level: int
|
6531
|
+
# - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
|
6532
|
+
# - lineno: int - May be 0.
|
6533
|
+
# - msg: str
|
6534
|
+
# - args: tuple | dict | 1-tuple[dict]
|
6535
|
+
# - exc_info: LoggingExcInfoTuple | None
|
6536
|
+
# - func: str | None = None -> funcName
|
6537
|
+
# - sinfo: str | None = None -> stack_info
|
6538
|
+
|
6539
|
+
def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
|
6540
|
+
for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
|
6541
|
+
self.__dict__.update(ad.context_to_record(_logging_context))
|
6243
6542
|
|
6244
6543
|
|
6245
6544
|
########################################
|
6246
|
-
# ../../../../../omlish/logs/std/
|
6545
|
+
# ../../../../../omlish/logs/std/loggers.py
|
6247
6546
|
|
6248
6547
|
|
6249
6548
|
##
|
@@ -6259,25 +6558,27 @@ class StdLogger(Logger):
|
|
6259
6558
|
def std(self) -> logging.Logger:
|
6260
6559
|
return self._std
|
6261
6560
|
|
6561
|
+
def is_enabled_for(self, level: LogLevel) -> bool:
|
6562
|
+
return self._std.isEnabledFor(level)
|
6563
|
+
|
6262
6564
|
def get_effective_level(self) -> LogLevel:
|
6263
6565
|
return self._std.getEffectiveLevel()
|
6264
6566
|
|
6265
6567
|
def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
|
6266
|
-
if not self.is_enabled_for(ctx.level):
|
6568
|
+
if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
|
6267
6569
|
return
|
6268
6570
|
|
6269
|
-
ctx.
|
6270
|
-
|
6271
|
-
ms, args = self._prepare_msg_args(msg, *args)
|
6272
|
-
|
6273
|
-
rec = LoggingContextLogRecord(
|
6571
|
+
ctx.set_basic(
|
6274
6572
|
name=self._std.name,
|
6275
|
-
msg=ms,
|
6276
|
-
args=args,
|
6277
6573
|
|
6278
|
-
|
6574
|
+
msg=msg,
|
6575
|
+
args=args,
|
6279
6576
|
)
|
6280
6577
|
|
6578
|
+
ctx.capture()
|
6579
|
+
|
6580
|
+
rec = LoggingContextLogRecord(_logging_context=ctx)
|
6581
|
+
|
6281
6582
|
self._std.handle(rec)
|
6282
6583
|
|
6283
6584
|
|