ominfra 0.0.0.dev430__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.
@@ -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/contexts.py
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/base.py
101
- LoggingMsgFn = ta.Callable[[], ta.Union[str, tuple]] # ta.TypeAlias
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')
@@ -2689,124 +2691,6 @@ def format_num_bytes(num_bytes: int) -> str:
2689
2691
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
2690
2692
 
2691
2693
 
2692
- ########################################
2693
- # ../../../../../omlish/logs/infos.py
2694
-
2695
-
2696
- ##
2697
-
2698
-
2699
- def logging_context_info(cls):
2700
- return cls
2701
-
2702
-
2703
- ##
2704
-
2705
-
2706
- @logging_context_info
2707
- @ta.final
2708
- class LoggingSourceFileInfo(ta.NamedTuple):
2709
- file_name: str
2710
- module: str
2711
-
2712
- @classmethod
2713
- def build(cls, file_path: ta.Optional[str]) -> ta.Optional['LoggingSourceFileInfo']:
2714
- if file_path is None:
2715
- return None
2716
-
2717
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L331-L336 # noqa
2718
- try:
2719
- file_name = os.path.basename(file_path)
2720
- module = os.path.splitext(file_name)[0]
2721
- except (TypeError, ValueError, AttributeError):
2722
- return None
2723
-
2724
- return cls(
2725
- file_name=file_name,
2726
- module=module,
2727
- )
2728
-
2729
-
2730
- ##
2731
-
2732
-
2733
- @logging_context_info
2734
- @ta.final
2735
- class LoggingThreadInfo(ta.NamedTuple):
2736
- ident: int
2737
- native_id: ta.Optional[int]
2738
- name: str
2739
-
2740
- @classmethod
2741
- def build(cls) -> 'LoggingThreadInfo':
2742
- return cls(
2743
- ident=threading.get_ident(),
2744
- native_id=threading.get_native_id() if hasattr(threading, 'get_native_id') else None,
2745
- name=threading.current_thread().name,
2746
- )
2747
-
2748
-
2749
- ##
2750
-
2751
-
2752
- @logging_context_info
2753
- @ta.final
2754
- class LoggingProcessInfo(ta.NamedTuple):
2755
- pid: int
2756
-
2757
- @classmethod
2758
- def build(cls) -> 'LoggingProcessInfo':
2759
- return cls(
2760
- pid=os.getpid(),
2761
- )
2762
-
2763
-
2764
- ##
2765
-
2766
-
2767
- @logging_context_info
2768
- @ta.final
2769
- class LoggingMultiprocessingInfo(ta.NamedTuple):
2770
- process_name: str
2771
-
2772
- @classmethod
2773
- def build(cls) -> ta.Optional['LoggingMultiprocessingInfo']:
2774
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L355-L364 # noqa
2775
- if (mp := sys.modules.get('multiprocessing')) is None:
2776
- return None
2777
-
2778
- return cls(
2779
- process_name=mp.current_process().name,
2780
- )
2781
-
2782
-
2783
- ##
2784
-
2785
-
2786
- @logging_context_info
2787
- @ta.final
2788
- class LoggingAsyncioTaskInfo(ta.NamedTuple):
2789
- name: str
2790
-
2791
- @classmethod
2792
- def build(cls) -> ta.Optional['LoggingAsyncioTaskInfo']:
2793
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L372-L377 # noqa
2794
- if (asyncio := sys.modules.get('asyncio')) is None:
2795
- return None
2796
-
2797
- try:
2798
- task = asyncio.current_task()
2799
- except Exception: # noqa
2800
- return None
2801
-
2802
- if task is None:
2803
- return None
2804
-
2805
- return cls(
2806
- name=task.get_name(), # Always non-None
2807
- )
2808
-
2809
-
2810
2694
  ########################################
2811
2695
  # ../../../../../omlish/logs/levels.py
2812
2696
 
@@ -4945,74 +4829,362 @@ def check_lite_runtime_version() -> None:
4945
4829
 
4946
4830
 
4947
4831
  ########################################
4948
- # ../../../../../omlish/logs/callers.py
4832
+ # ../../../../../omlish/logs/infos.py
4833
+ """
4834
+ TODO:
4835
+ - remove redundant info fields only present for std adaptation (Level.name, ...)
4836
+ """
4949
4837
 
4950
4838
 
4951
4839
  ##
4952
4840
 
4953
4841
 
4954
- @logging_context_info
4842
+ def logging_context_info(cls):
4843
+ return cls
4844
+
4845
+
4955
4846
  @ta.final
4956
- class LoggingCaller(ta.NamedTuple):
4957
- file_path: str
4958
- line_no: int
4959
- name: str
4960
- stack_info: ta.Optional[str]
4847
+ class LoggingContextInfos:
4848
+ def __new__(cls, *args, **kwargs): # noqa
4849
+ raise TypeError
4961
4850
 
4962
- @classmethod
4963
- def is_internal_frame(cls, frame: types.FrameType) -> bool:
4964
- file_path = os.path.normcase(frame.f_code.co_filename)
4851
+ #
4965
4852
 
4966
- # Yes, really.
4967
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L204
4968
- # https://github.com/python/cpython/commit/5ca6d7469be53960843df39bb900e9c3359f127f
4969
- if 'importlib' in file_path and '_bootstrap' in file_path:
4970
- return True
4853
+ @logging_context_info
4854
+ @ta.final
4855
+ class Name(ta.NamedTuple):
4856
+ name: str
4971
4857
 
4972
- return False
4858
+ @logging_context_info
4859
+ @ta.final
4860
+ class Level(ta.NamedTuple):
4861
+ level: NamedLogLevel
4862
+ name: str
4973
4863
 
4974
- @classmethod
4975
- def find_frame(cls, ofs: int = 0) -> ta.Optional[types.FrameType]:
4976
- f: ta.Optional[types.FrameType] = sys._getframe(2 + ofs) # noqa
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
+ )
4871
+
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]
4977
4877
 
4978
- while f is not None:
4979
- # NOTE: We don't check __file__ like stdlib since we may be running amalgamated - we rely on careful, manual
4980
- # stack_offset management.
4981
- if hasattr(f, 'f_code'):
4982
- return f
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)
4983
4900
 
4984
- f = f.f_back
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 = '', ()
4985
4908
 
4986
- return None
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
4987
5048
 
4988
- @classmethod
4989
- def find(
4990
- cls,
4991
- ofs: int = 0,
4992
- *,
4993
- stack_info: bool = False,
4994
- ) -> ta.Optional['LoggingCaller']:
4995
- if (f := cls.find_frame(ofs + 1)) is None:
4996
5049
  return None
4997
5050
 
4998
- # https://github.com/python/cpython/blob/08e9794517063c8cd92c48714071b1d3c60b71bd/Lib/logging/__init__.py#L1616-L1623 # noqa
4999
- sinfo = None
5000
- if stack_info:
5001
- sio = io.StringIO()
5002
- traceback.print_stack(f, file=sio)
5003
- sinfo = sio.getvalue()
5004
- sio.close()
5005
- if sinfo[-1] == '\n':
5006
- sinfo = sinfo[:-1]
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
5060
+
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
+ )
5007
5077
 
5008
- return cls(
5009
- file_path=f.f_code.co_filename,
5010
- line_no=f.f_lineno or 0,
5011
- name=f.f_code.co_name,
5012
- stack_info=sinfo,
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,
5013
5182
  )
5014
5183
 
5015
5184
 
5185
+ _check_logging_start_time()
5186
+
5187
+
5016
5188
  ########################################
5017
5189
  # ../../../../../omlish/logs/std/json.py
5018
5190
  """
@@ -5071,131 +5243,46 @@ class JsonLoggingFormatter(logging.Formatter):
5071
5243
 
5072
5244
 
5073
5245
  ########################################
5074
- # ../../../../../omlish/logs/times.py
5246
+ # ../../logs.py
5247
+ """
5248
+ https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html :
5249
+ - The maximum batch size is 1,048,576 bytes. This size is calculated as the sum of all event messages in UTF-8, plus 26
5250
+ bytes for each log event.
5251
+ - None of the log events in the batch can be more than 2 hours in the future.
5252
+ - None of the log events in the batch can be more than 14 days in the past. Also, none of the log events can be from
5253
+ earlier than the retention period of the log group.
5254
+ - The log events in the batch must be in chronological order by their timestamp. The timestamp is the time that the
5255
+ event occurred, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. (In AWS Tools for PowerShell
5256
+ and the AWS SDK for .NET, the timestamp is specified in .NET format: yyyy-mm-ddThh:mm:ss. For example,
5257
+ 2017-09-15T13:45:30.)
5258
+ - A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails.
5259
+ - Each log event can be no larger than 256 KB.
5260
+ - The maximum number of log events in a batch is 10,000.
5261
+ """
5075
5262
 
5076
5263
 
5077
5264
  ##
5078
5265
 
5079
5266
 
5080
- @logging_context_info
5081
- @ta.final
5082
- class LoggingTimeFields(ta.NamedTuple):
5083
- """Maps directly to stdlib `logging.LogRecord` fields, and must be kept in sync with it."""
5267
+ @dc.dataclass(frozen=True)
5268
+ class AwsLogEvent(AwsDataclass):
5269
+ message: str
5270
+ timestamp: int # milliseconds UTC
5084
5271
 
5085
- created: float
5086
- msecs: float
5087
- relative_created: float
5088
5272
 
5089
- @classmethod
5090
- def get_std_start_time_ns(cls) -> int:
5091
- x: ta.Any = logging._startTime # type: ignore[attr-defined] # noqa
5273
+ @dc.dataclass(frozen=True)
5274
+ class AwsPutLogEventsRequest(AwsDataclass):
5275
+ log_group_name: str
5276
+ log_stream_name: str
5277
+ log_events: ta.Sequence[AwsLogEvent]
5278
+ sequence_token: ta.Optional[str] = None
5092
5279
 
5093
- # Before 3.13.0b1 this will be `time.time()`, a float of seconds. After that, it will be `time.time_ns()`, an
5094
- # int.
5095
- #
5096
- # See:
5097
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5098
- #
5099
- if isinstance(x, float):
5100
- return int(x * 1e9)
5101
- else:
5102
- return x
5103
5280
 
5104
- @classmethod
5105
- def build(
5106
- cls,
5107
- time_ns: int,
5108
- *,
5109
- start_time_ns: ta.Optional[int] = None,
5110
- ) -> 'LoggingTimeFields':
5111
- # https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
5112
- created = time_ns / 1e9 # ns to float seconds
5113
-
5114
- # Get the number of whole milliseconds (0-999) in the fractional part of seconds.
5115
- # Eg: 1_677_903_920_999_998_503 ns --> 999_998_503 ns--> 999 ms
5116
- # Convert to float by adding 0.0 for historical reasons. See gh-89047
5117
- msecs = (time_ns % 1_000_000_000) // 1_000_000 + 0.0
5118
-
5119
- # https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
5120
- if msecs == 999.0 and int(created) != time_ns // 1_000_000_000:
5121
- # ns -> sec conversion can round up, e.g:
5122
- # 1_677_903_920_999_999_900 ns --> 1_677_903_921.0 sec
5123
- msecs = 0.0
5124
-
5125
- if start_time_ns is None:
5126
- start_time_ns = cls.get_std_start_time_ns()
5127
- relative_created = (time_ns - start_time_ns) / 1e6
5128
-
5129
- return cls(
5130
- created=created,
5131
- msecs=msecs,
5132
- relative_created=relative_created,
5133
- )
5134
-
5135
-
5136
- ##
5137
-
5138
-
5139
- class UnexpectedLoggingStartTimeWarning(LoggingSetupWarning):
5140
- pass
5141
-
5142
-
5143
- def _check_logging_start_time() -> None:
5144
- if (x := LoggingTimeFields.get_std_start_time_ns()) < (t := time.time()):
5145
- import warnings # noqa
5146
-
5147
- warnings.warn(
5148
- f'Unexpected logging start time detected: '
5149
- f'get_std_start_time_ns={x}, '
5150
- f'time.time()={t}',
5151
- UnexpectedLoggingStartTimeWarning,
5152
- )
5153
-
5154
-
5155
- _check_logging_start_time()
5156
-
5157
-
5158
- ########################################
5159
- # ../../logs.py
5160
- """
5161
- https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html :
5162
- - The maximum batch size is 1,048,576 bytes. This size is calculated as the sum of all event messages in UTF-8, plus 26
5163
- bytes for each log event.
5164
- - None of the log events in the batch can be more than 2 hours in the future.
5165
- - None of the log events in the batch can be more than 14 days in the past. Also, none of the log events can be from
5166
- earlier than the retention period of the log group.
5167
- - The log events in the batch must be in chronological order by their timestamp. The timestamp is the time that the
5168
- event occurred, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. (In AWS Tools for PowerShell
5169
- and the AWS SDK for .NET, the timestamp is specified in .NET format: yyyy-mm-ddThh:mm:ss. For example,
5170
- 2017-09-15T13:45:30.)
5171
- - A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails.
5172
- - Each log event can be no larger than 256 KB.
5173
- - The maximum number of log events in a batch is 10,000.
5174
- """
5175
-
5176
-
5177
- ##
5178
-
5179
-
5180
- @dc.dataclass(frozen=True)
5181
- class AwsLogEvent(AwsDataclass):
5182
- message: str
5183
- timestamp: int # milliseconds UTC
5184
-
5185
-
5186
- @dc.dataclass(frozen=True)
5187
- class AwsPutLogEventsRequest(AwsDataclass):
5188
- log_group_name: str
5189
- log_stream_name: str
5190
- log_events: ta.Sequence[AwsLogEvent]
5191
- sequence_token: ta.Optional[str] = None
5192
-
5193
-
5194
- @dc.dataclass(frozen=True)
5195
- class AwsRejectedLogEventsInfo(AwsDataclass):
5196
- expired_log_event_end_index: ta.Optional[int] = None
5197
- too_new_log_event_start_index: ta.Optional[int] = None
5198
- too_old_log_event_end_index: ta.Optional[int] = None
5281
+ @dc.dataclass(frozen=True)
5282
+ class AwsRejectedLogEventsInfo(AwsDataclass):
5283
+ expired_log_event_end_index: ta.Optional[int] = None
5284
+ too_new_log_event_start_index: ta.Optional[int] = None
5285
+ too_old_log_event_end_index: ta.Optional[int] = None
5199
5286
 
5200
5287
 
5201
5288
  @dc.dataclass(frozen=True)
@@ -5366,68 +5453,36 @@ def load_config_file_obj(
5366
5453
 
5367
5454
 
5368
5455
  class LoggingContext(Abstract):
5369
- @property
5370
- @abc.abstractmethod
5371
- def level(self) -> NamedLogLevel:
5372
- raise NotImplementedError
5373
-
5374
- #
5375
-
5376
- @property
5377
5456
  @abc.abstractmethod
5378
- def time_ns(self) -> int:
5457
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5379
5458
  raise NotImplementedError
5380
5459
 
5381
- @property
5382
- @abc.abstractmethod
5383
- def times(self) -> LoggingTimeFields:
5384
- raise NotImplementedError
5385
-
5386
- #
5460
+ @ta.final
5461
+ def __getitem__(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5462
+ return self.get_info(ty)
5387
5463
 
5388
- @property
5389
- @abc.abstractmethod
5390
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
5391
- raise NotImplementedError
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
5392
5469
 
5393
- @property
5394
- @abc.abstractmethod
5395
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
5396
- raise NotImplementedError
5470
+ ##
5397
5471
 
5398
- #
5399
5472
 
5473
+ class CaptureLoggingContext(LoggingContext, Abstract):
5400
5474
  @abc.abstractmethod
5401
- def caller(self) -> ta.Optional[LoggingCaller]:
5402
- raise NotImplementedError
5475
+ def set_basic(
5476
+ self,
5477
+ name: str,
5403
5478
 
5404
- @abc.abstractmethod
5405
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
5479
+ msg: ta.Union[str, tuple, LoggingMsgFn],
5480
+ args: tuple,
5481
+ ) -> 'CaptureLoggingContext':
5406
5482
  raise NotImplementedError
5407
5483
 
5408
5484
  #
5409
5485
 
5410
- @abc.abstractmethod
5411
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
5412
- raise NotImplementedError
5413
-
5414
- @abc.abstractmethod
5415
- def process(self) -> ta.Optional[LoggingProcessInfo]:
5416
- raise NotImplementedError
5417
-
5418
- @abc.abstractmethod
5419
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
5420
- raise NotImplementedError
5421
-
5422
- @abc.abstractmethod
5423
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
5424
- raise NotImplementedError
5425
-
5426
-
5427
- ##
5428
-
5429
-
5430
- class CaptureLoggingContext(LoggingContext, Abstract):
5431
5486
  class AlreadyCapturedError(Exception):
5432
5487
  pass
5433
5488
 
@@ -5458,80 +5513,50 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
5458
5513
 
5459
5514
  exc_info: LoggingExcInfoArg = False,
5460
5515
 
5461
- caller: ta.Union[LoggingCaller, ta.Type[NOT_SET], None] = NOT_SET,
5516
+ caller: ta.Union[LoggingContextInfos.Caller, ta.Type[NOT_SET], None] = NOT_SET,
5462
5517
  stack_offset: int = 0,
5463
5518
  stack_info: bool = False,
5464
5519
  ) -> None:
5465
- self._level: NamedLogLevel = level if level.__class__ is NamedLogLevel else NamedLogLevel(level) # type: ignore[assignment] # noqa
5466
-
5467
- #
5520
+ # TODO: Name, Msg, Extra
5468
5521
 
5469
5522
  if time_ns is None:
5470
5523
  time_ns = time.time_ns()
5471
- self._time_ns: int = time_ns
5472
5524
 
5473
- #
5474
-
5475
- if exc_info is True:
5476
- sys_exc_info = sys.exc_info()
5477
- if sys_exc_info[0] is not None:
5478
- exc_info = sys_exc_info
5479
- else:
5480
- exc_info = None
5481
- elif exc_info is False:
5482
- exc_info = None
5483
-
5484
- if exc_info is not None:
5485
- self._exc_info: ta.Optional[LoggingExcInfo] = exc_info
5486
- if isinstance(exc_info, BaseException):
5487
- self._exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = (type(exc_info), exc_info, exc_info.__traceback__) # noqa
5488
- else:
5489
- self._exc_info_tuple = exc_info
5490
-
5491
- #
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
+ )
5492
5531
 
5493
5532
  if caller is not CaptureLoggingContextImpl.NOT_SET:
5494
- self._caller = caller # type: ignore[assignment]
5533
+ self._infos[LoggingContextInfos.Caller] = caller
5495
5534
  else:
5496
5535
  self._stack_offset = stack_offset
5497
5536
  self._stack_info = stack_info
5498
5537
 
5499
- ##
5500
-
5501
- @property
5502
- def level(self) -> NamedLogLevel:
5503
- return self._level
5504
-
5505
- #
5506
-
5507
- @property
5508
- def time_ns(self) -> int:
5509
- return self._time_ns
5510
-
5511
- _times: LoggingTimeFields
5512
-
5513
- @property
5514
- def times(self) -> LoggingTimeFields:
5515
- try:
5516
- return self._times
5517
- except AttributeError:
5518
- pass
5519
-
5520
- times = self._times = LoggingTimeFields.build(self.time_ns)
5521
- return times
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
5522
5543
 
5523
- #
5544
+ def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
5545
+ return self._infos.get(ty)
5524
5546
 
5525
- _exc_info: ta.Optional[LoggingExcInfo] = None
5526
- _exc_info_tuple: ta.Optional[LoggingExcInfoTuple] = None
5547
+ ##
5527
5548
 
5528
- @property
5529
- def exc_info(self) -> ta.Optional[LoggingExcInfo]:
5530
- return self._exc_info
5549
+ def set_basic(
5550
+ self,
5551
+ name: str,
5531
5552
 
5532
- @property
5533
- def exc_info_tuple(self) -> ta.Optional[LoggingExcInfoTuple]:
5534
- return self._exc_info_tuple
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
+ )
5535
5560
 
5536
5561
  ##
5537
5562
 
@@ -5545,74 +5570,28 @@ class CaptureLoggingContextImpl(CaptureLoggingContext):
5545
5570
 
5546
5571
  _has_captured: bool = False
5547
5572
 
5548
- _caller: ta.Optional[LoggingCaller]
5549
- _source_file: ta.Optional[LoggingSourceFileInfo]
5550
-
5551
- _thread: ta.Optional[LoggingThreadInfo]
5552
- _process: ta.Optional[LoggingProcessInfo]
5553
- _multiprocessing: ta.Optional[LoggingMultiprocessingInfo]
5554
- _asyncio_task: ta.Optional[LoggingAsyncioTaskInfo]
5555
-
5556
5573
  def capture(self) -> None:
5557
5574
  if self._has_captured:
5558
5575
  raise CaptureLoggingContextImpl.AlreadyCapturedError
5559
5576
  self._has_captured = True
5560
5577
 
5561
- if not hasattr(self, '_caller'):
5562
- self._caller = LoggingCaller.find(
5578
+ if LoggingContextInfos.Caller not in self._infos:
5579
+ self._set_info(LoggingContextInfos.Caller.build(
5563
5580
  self._stack_offset + 1,
5564
5581
  stack_info=self._stack_info,
5565
- )
5566
-
5567
- if (caller := self._caller) is not None:
5568
- self._source_file = LoggingSourceFileInfo.build(caller.file_path)
5569
- else:
5570
- self._source_file = None
5571
-
5572
- self._thread = LoggingThreadInfo.build()
5573
- self._process = LoggingProcessInfo.build()
5574
- self._multiprocessing = LoggingMultiprocessingInfo.build()
5575
- self._asyncio_task = LoggingAsyncioTaskInfo.build()
5576
-
5577
- #
5578
-
5579
- def caller(self) -> ta.Optional[LoggingCaller]:
5580
- try:
5581
- return self._caller
5582
- except AttributeError:
5583
- raise CaptureLoggingContext.NotCapturedError from None
5584
-
5585
- def source_file(self) -> ta.Optional[LoggingSourceFileInfo]:
5586
- try:
5587
- return self._source_file
5588
- except AttributeError:
5589
- raise CaptureLoggingContext.NotCapturedError from None
5590
-
5591
- #
5592
-
5593
- def thread(self) -> ta.Optional[LoggingThreadInfo]:
5594
- try:
5595
- return self._thread
5596
- except AttributeError:
5597
- raise CaptureLoggingContext.NotCapturedError from None
5598
-
5599
- def process(self) -> ta.Optional[LoggingProcessInfo]:
5600
- try:
5601
- return self._process
5602
- except AttributeError:
5603
- raise CaptureLoggingContext.NotCapturedError from None
5582
+ ))
5604
5583
 
5605
- def multiprocessing(self) -> ta.Optional[LoggingMultiprocessingInfo]:
5606
- try:
5607
- return self._multiprocessing
5608
- except AttributeError:
5609
- 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
+ ))
5610
5588
 
5611
- def asyncio_task(self) -> ta.Optional[LoggingAsyncioTaskInfo]:
5612
- try:
5613
- return self._asyncio_task
5614
- except AttributeError:
5615
- raise CaptureLoggingContext.NotCapturedError from None
5589
+ self._set_info(
5590
+ LoggingContextInfos.Thread.build(),
5591
+ LoggingContextInfos.Process.build(),
5592
+ LoggingContextInfos.Multiprocessing.build(),
5593
+ LoggingContextInfos.AsyncioTask.build(),
5594
+ )
5616
5595
 
5617
5596
 
5618
5597
  ########################################
@@ -5917,36 +5896,6 @@ class AnyLogger(Abstract, ta.Generic[T]):
5917
5896
 
5918
5897
  ##
5919
5898
 
5920
- @classmethod
5921
- def _prepare_msg_args(cls, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> ta.Tuple[str, tuple]:
5922
- if callable(msg):
5923
- if args:
5924
- raise TypeError(f'Must not provide both a message function and args: {msg=} {args=}')
5925
- x = msg()
5926
- if isinstance(x, str):
5927
- return x, ()
5928
- elif isinstance(x, tuple):
5929
- if x:
5930
- return x[0], x[1:]
5931
- else:
5932
- return '', ()
5933
- else:
5934
- raise TypeError(x)
5935
-
5936
- elif isinstance(msg, tuple):
5937
- if args:
5938
- raise TypeError(f'Must not provide both a tuple message and args: {msg=} {args=}')
5939
- if msg:
5940
- return msg[0], msg[1:]
5941
- else:
5942
- return '', ()
5943
-
5944
- elif isinstance(msg, str):
5945
- return msg, args
5946
-
5947
- else:
5948
- raise TypeError(msg)
5949
-
5950
5899
  @abc.abstractmethod
5951
5900
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any, **kwargs: ta.Any) -> T: # noqa
5952
5901
  raise NotImplementedError
@@ -5987,137 +5936,543 @@ class AsyncNopLogger(AnyNopLogger[ta.Awaitable[None]], AsyncLogger):
5987
5936
 
5988
5937
  ########################################
5989
5938
  # ../../../../../omlish/logs/std/records.py
5939
+ """
5940
+ TODO:
5941
+ - TypedDict?
5942
+ """
5990
5943
 
5991
5944
 
5992
5945
  ##
5993
5946
 
5994
5947
 
5995
- # Ref:
5996
- # - https://docs.python.org/3/library/logging.html#logrecord-attributes
5997
- #
5998
- # LogRecord:
5999
- # - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8)
6000
- # - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
6001
- #
6002
- # LogRecord.__init__ args:
6003
- # - name: str
6004
- # - level: int
6005
- # - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
6006
- # - lineno: int - May be 0.
6007
- # - msg: str
6008
- # - args: tuple | dict | 1-tuple[dict]
6009
- # - exc_info: LoggingExcInfoTuple | None
6010
- # - func: str | None = None -> funcName
6011
- # - sinfo: str | None = None -> stack_info
6012
- #
6013
- KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6014
- # Name of the logger used to log the call. Unmodified by ctor.
6015
- 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
+ #
6016
5956
 
6017
- # The format string passed in the original logging call. Merged with args to produce message, or an arbitrary object
6018
- # (see Using arbitrary objects as messages). Unmodified by ctor.
6019
- msg=str,
5957
+ def __new__(cls, *args, **kwargs): # noqa
5958
+ raise TypeError
6020
5959
 
6021
- # The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge (when
6022
- # there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a Mapping into just
6023
- # the mapping, but is otherwise unmodified.
6024
- args=ta.Union[tuple, dict],
5960
+ class Adapter(Abstract, ta.Generic[T]):
5961
+ @property
5962
+ @abc.abstractmethod
5963
+ def info_cls(self) -> ta.Type[LoggingContextInfo]:
5964
+ raise NotImplementedError
6025
5965
 
6026
- #
5966
+ #
6027
5967
 
6028
- # Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
6029
- # `getLevelName(level)`.
6030
- levelname=str,
5968
+ @ta.final
5969
+ class NOT_SET: # noqa
5970
+ def __new__(cls, *args, **kwargs): # noqa
5971
+ raise TypeError
6031
5972
 
6032
- # Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
6033
- levelno=int,
5973
+ class RecordAttr(ta.NamedTuple):
5974
+ name: str
5975
+ type: ta.Any
5976
+ default: ta.Any
6034
5977
 
6035
- #
5978
+ # @abc.abstractmethod
5979
+ record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
6036
5980
 
6037
- # Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May default
6038
- # to "(unknown file)" by Logger.findCaller / Logger._log.
6039
- pathname=str,
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
6040
5988
 
6041
- # Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to pathname.
6042
- filename=str,
5989
+ #
6043
5990
 
6044
- # Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
6045
- # "Unknown module".
6046
- module=str,
5991
+ @abc.abstractmethod
5992
+ def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
5993
+ raise NotImplementedError
6047
5994
 
6048
- #
5995
+ #
6049
5996
 
6050
- # Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
6051
- exc_info=ta.Optional[LoggingExcInfoTuple],
5997
+ @abc.abstractmethod
5998
+ def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
5999
+ raise NotImplementedError
6052
6000
 
6053
- # Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
6054
- exc_text=ta.Optional[str],
6001
+ #
6055
6002
 
6056
- #
6003
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6004
+ super().__init_subclass__(**kwargs)
6057
6005
 
6058
- # Stack frame information (where available) from the bottom of the stack in the current thread, up to and including
6059
- # the stack frame of the logging call which resulted in the creation of this record. Set by ctor to `sinfo` arg,
6060
- # unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
6061
- # the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
6062
- stack_info=ta.Optional[str],
6006
+ if Abstract in cls.__bases__:
6007
+ return
6063
6008
 
6064
- # Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0 by
6065
- # Logger.findCaller / Logger._log.
6066
- lineno=int,
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
6067
6028
 
6068
- # Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
6069
- # "(unknown function)" by Logger.findCaller / Logger._log.
6070
- funcName=str,
6029
+ class RequiredAdapter(Adapter[T], Abstract):
6030
+ @property
6031
+ @abc.abstractmethod
6032
+ def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
6033
+ raise NotImplementedError
6071
6034
 
6072
- #
6035
+ #
6073
6036
 
6074
- # Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply `time.time()`.
6075
- #
6076
- # See:
6077
- # - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
6078
- # - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
6079
- #
6080
- 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?
6081
6043
 
6082
- # Millisecond portion of the time when the LogRecord was created.
6083
- msecs=float,
6044
+ @abc.abstractmethod
6045
+ def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
6046
+ raise NotImplementedError
6084
6047
 
6085
- # Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
6086
- relativeCreated=float,
6048
+ #
6087
6049
 
6088
- #
6050
+ @abc.abstractmethod
6051
+ def record_to_info(self, rec: logging.LogRecord) -> T:
6052
+ raise NotImplementedError
6089
6053
 
6090
- # Thread ID if available, and `logging.logThreads` is truthy.
6091
- thread=ta.Optional[int],
6054
+ #
6092
6055
 
6093
- # Thread name if available, and `logging.logThreads` is truthy.
6094
- threadName=ta.Optional[str],
6056
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
6057
+ super().__init_subclass__(**kwargs)
6095
6058
 
6096
- #
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
6097
6092
 
6098
- # Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
6099
- # 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise remains
6100
- # as 'MainProcess'.
6101
6093
  #
6102
- # As noted by stdlib:
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()
6103
6163
  #
6104
- # Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
6105
- # third-party code to run when multiprocessing calls import. See issue 8200 for an example
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()
6106
6172
  #
6107
- processName=ta.Optional[str],
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
+ )
6108
6195
 
6109
- # Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy, otherwise
6110
- # None.
6111
- process=ta.Optional[int],
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
+ )
6112
6202
 
6113
- #
6203
+ def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
6204
+ return LoggingContextInfos.Time.build(
6205
+ int(rec.created * 1e9),
6206
+ )
6114
6207
 
6115
- # Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
6116
- # `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
6117
- taskName=ta.Optional[str],
6118
- )
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]
6119
6231
 
6120
- KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_RECORD_ATTRS)
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
6361
+
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
+ )
6121
6476
 
6122
6477
 
6123
6478
  # Formatter:
@@ -6141,14 +6496,17 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
6141
6496
  KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
6142
6497
 
6143
6498
 
6144
- ##
6145
-
6146
-
6147
6499
  class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
6148
6500
  pass
6149
6501
 
6150
6502
 
6151
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
+
6152
6510
  rec_dct = dict(logging.makeLogRecord({}).__dict__)
6153
6511
 
6154
6512
  if (unk_rec_fields := frozenset(rec_dct) - KNOWN_STD_LOGGING_RECORD_ATTR_SET):
@@ -6167,116 +6525,20 @@ _check_std_logging_record_attrs()
6167
6525
 
6168
6526
 
6169
6527
  class LoggingContextLogRecord(logging.LogRecord):
6170
- _SHOULD_ADD_TASK_NAME: ta.ClassVar[bool] = sys.version_info >= (3, 12)
6171
-
6172
- _UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
6173
- _UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
6174
- _UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
6175
-
6176
- _STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
6177
-
6178
- def __init__( # noqa
6179
- self,
6180
- # name,
6181
- # level,
6182
- # pathname,
6183
- # lineno,
6184
- # msg,
6185
- # args,
6186
- # exc_info,
6187
- # func=None,
6188
- # sinfo=None,
6189
- # **kwargs,
6190
- *,
6191
- name: str,
6192
- msg: str,
6193
- args: ta.Union[tuple, dict],
6194
-
6195
- _logging_context: LoggingContext,
6196
- ) -> None:
6197
- ctx = _logging_context
6198
-
6199
- self.name: str = name
6200
-
6201
- self.msg: str = msg
6202
-
6203
- # https://github.com/python/cpython/blob/e709361fc87d0d9ab9c58033a0a7f2fef0ad43d2/Lib/logging/__init__.py#L307
6204
- if args and len(args) == 1 and isinstance(args[0], collections.abc.Mapping) and args[0]:
6205
- args = args[0] # type: ignore[assignment]
6206
- self.args: ta.Union[tuple, dict] = args
6207
-
6208
- self.levelname: str = logging.getLevelName(ctx.level)
6209
- self.levelno: int = ctx.level
6210
-
6211
- if (caller := ctx.caller()) is not None:
6212
- self.pathname: str = caller.file_path
6213
- else:
6214
- self.pathname = self._UNKNOWN_PATH_NAME
6215
-
6216
- if (src_file := ctx.source_file()) is not None:
6217
- self.filename: str = src_file.file_name
6218
- self.module: str = src_file.module
6219
- else:
6220
- self.filename = self.pathname
6221
- self.module = self._UNKNOWN_MODULE
6222
-
6223
- self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
6224
- self.exc_text: ta.Optional[str] = None
6225
-
6226
- # If ctx.build_caller() was never called, we simply don't have a stack trace.
6227
- if caller is not None:
6228
- if (sinfo := caller.stack_info) is not None:
6229
- self.stack_info: ta.Optional[str] = '\n'.join([
6230
- self._STACK_INFO_PREFIX,
6231
- sinfo[1:] if sinfo.endswith('\n') else sinfo,
6232
- ])
6233
- else:
6234
- self.stack_info = None
6235
-
6236
- self.lineno: int = caller.line_no
6237
- self.funcName: str = caller.name
6238
-
6239
- else:
6240
- self.stack_info = None
6241
-
6242
- self.lineno = 0
6243
- self.funcName = self._UNKNOWN_FUNC_NAME
6244
-
6245
- times = ctx.times
6246
- self.created: float = times.created
6247
- self.msecs: float = times.msecs
6248
- self.relativeCreated: float = times.relative_created
6249
-
6250
- if logging.logThreads:
6251
- thread = check.not_none(ctx.thread())
6252
- self.thread: ta.Optional[int] = thread.ident
6253
- self.threadName: ta.Optional[str] = thread.name
6254
- else:
6255
- self.thread = None
6256
- self.threadName = None
6257
-
6258
- if logging.logProcesses:
6259
- process = check.not_none(ctx.process())
6260
- self.process: ta.Optional[int] = process.pid
6261
- else:
6262
- self.process = None
6263
-
6264
- if logging.logMultiprocessing:
6265
- if (mp := ctx.multiprocessing()) is not None:
6266
- self.processName: ta.Optional[str] = mp.process_name
6267
- else:
6268
- self.processName = None
6269
- else:
6270
- self.processName = None
6271
-
6272
- # Absent <3.12
6273
- if getattr(logging, 'logAsyncioTasks', None):
6274
- if (at := ctx.asyncio_task()) is not None:
6275
- self.taskName: ta.Optional[str] = at.name
6276
- else:
6277
- self.taskName = None
6278
- else:
6279
- 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))
6280
6542
 
6281
6543
 
6282
6544
  ########################################
@@ -6303,21 +6565,20 @@ class StdLogger(Logger):
6303
6565
  return self._std.getEffectiveLevel()
6304
6566
 
6305
6567
  def _log(self, ctx: CaptureLoggingContext, msg: ta.Union[str, tuple, LoggingMsgFn], *args: ta.Any) -> None:
6306
- if not self.is_enabled_for(ctx.level):
6568
+ if not self.is_enabled_for(ctx.must_get_info(LoggingContextInfos.Level).level):
6307
6569
  return
6308
6570
 
6309
- ctx.capture()
6310
-
6311
- ms, args = self._prepare_msg_args(msg, *args)
6312
-
6313
- rec = LoggingContextLogRecord(
6571
+ ctx.set_basic(
6314
6572
  name=self._std.name,
6315
- msg=ms,
6316
- args=args,
6317
6573
 
6318
- _logging_context=ctx,
6574
+ msg=msg,
6575
+ args=args,
6319
6576
  )
6320
6577
 
6578
+ ctx.capture()
6579
+
6580
+ rec = LoggingContextLogRecord(_logging_context=ctx)
6581
+
6321
6582
  self._std.handle(rec)
6322
6583
 
6323
6584