pyvesync 3.0.0__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.
- pyvesync/__init__.py +5 -0
- pyvesync/base_devices/__init__.py +35 -0
- pyvesync/base_devices/bulb_base.py +330 -0
- pyvesync/base_devices/fan_base.py +392 -0
- pyvesync/base_devices/fryer_base.py +68 -0
- pyvesync/base_devices/humidifier_base.py +462 -0
- pyvesync/base_devices/outlet_base.py +327 -0
- pyvesync/base_devices/purifier_base.py +525 -0
- pyvesync/base_devices/switch_base.py +271 -0
- pyvesync/base_devices/thermostat_base.py +283 -0
- pyvesync/base_devices/vesyncbasedevice.py +572 -0
- pyvesync/const.py +825 -0
- pyvesync/device_container.py +373 -0
- pyvesync/device_map.py +1172 -0
- pyvesync/devices/__init__.py +1 -0
- pyvesync/devices/vesyncbulb.py +1126 -0
- pyvesync/devices/vesyncfan.py +233 -0
- pyvesync/devices/vesynchumidifier.py +808 -0
- pyvesync/devices/vesynckitchen.py +651 -0
- pyvesync/devices/vesyncoutlet.py +954 -0
- pyvesync/devices/vesyncpurifier.py +990 -0
- pyvesync/devices/vesyncswitch.py +415 -0
- pyvesync/devices/vesyncthermostat.py +171 -0
- pyvesync/models/__init__.py +11 -0
- pyvesync/models/base_models.py +129 -0
- pyvesync/models/bulb_models.py +197 -0
- pyvesync/models/bypass_models.py +289 -0
- pyvesync/models/fan_models.py +47 -0
- pyvesync/models/fryer_models.py +34 -0
- pyvesync/models/home_models.py +169 -0
- pyvesync/models/humidifier_models.py +202 -0
- pyvesync/models/outlet_models.py +220 -0
- pyvesync/models/purifier_models.py +261 -0
- pyvesync/models/switch_models.py +101 -0
- pyvesync/models/thermostat_models.py +134 -0
- pyvesync/models/vesync_models.py +439 -0
- pyvesync/utils/__init__.py +1 -0
- pyvesync/utils/colors.py +250 -0
- pyvesync/utils/device_mixins.py +362 -0
- pyvesync/utils/enum_utils.py +26 -0
- pyvesync/utils/errors.py +867 -0
- pyvesync/utils/helpers.py +700 -0
- pyvesync/utils/logs.py +525 -0
- pyvesync/vesync.py +638 -0
- pyvesync/vesynchome.py +152 -0
- pyvesync-3.0.0.dist-info/METADATA +567 -0
- pyvesync-3.0.0.dist-info/RECORD +50 -0
- pyvesync-3.0.0.dist-info/WHEEL +5 -0
- pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
- pyvesync-3.0.0.dist-info/top_level.txt +1 -0
pyvesync/utils/logs.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""library_logger.py.
|
|
2
|
+
|
|
3
|
+
This module provides a custom logger for libraries. It automatically
|
|
4
|
+
captures the caller's module and class name in the log records, and it
|
|
5
|
+
provides helper methods for logging API calls and other specially formatted
|
|
6
|
+
messages.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from library_logger import LibraryLogger
|
|
10
|
+
|
|
11
|
+
# Use `LibraryLogger.log_api_call` to log API calls.
|
|
12
|
+
LibraryLogger.log_api_call(logger, response)
|
|
13
|
+
|
|
14
|
+
# Use `LibraryLogger.log_api_exception` to log API exceptions.
|
|
15
|
+
LibraryLogger.log_api_exception(logger, request_dict, exception)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
from dataclasses import fields, is_dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
import orjson
|
|
29
|
+
from mashumaro.exceptions import InvalidFieldValue, MissingField, UnserializableField
|
|
30
|
+
from multidict import CIMultiDictProxy
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from aiohttp import ClientResponse
|
|
34
|
+
from aiohttp.client_exceptions import ClientResponseError
|
|
35
|
+
|
|
36
|
+
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LibraryLogger:
|
|
40
|
+
"""Library Logging Interface.
|
|
41
|
+
|
|
42
|
+
LibraryLogger provides a logging interface that automatically adds context
|
|
43
|
+
(module and class name) and offers helper methods for structured logging,
|
|
44
|
+
such as API call logs.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
debug (bool): Class attribute to enable or disable debug logging,
|
|
48
|
+
prints API requests & responses that return an error.
|
|
49
|
+
shouldredact (bool): Class attribute whether to redact
|
|
50
|
+
sensitive information from logs.
|
|
51
|
+
verbose (bool): Class attribute to print all request & response content.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
Logging an API call:
|
|
55
|
+
>>> LibraryLogger.log_api_call(logger, response)
|
|
56
|
+
2025-02-01 12:34:56 - DEBUG - my_library - ========API CALL========
|
|
57
|
+
API CALL to endpoint: /api/v1/resource
|
|
58
|
+
Method: POST
|
|
59
|
+
Request Headers:
|
|
60
|
+
{
|
|
61
|
+
REQUEST HEADERS
|
|
62
|
+
}
|
|
63
|
+
Request Body:
|
|
64
|
+
{
|
|
65
|
+
"key": "value"
|
|
66
|
+
}
|
|
67
|
+
Response Body:
|
|
68
|
+
{
|
|
69
|
+
RESPONSE BODY
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Logging an API exception:
|
|
73
|
+
>>> LibraryLogger.log_api_exception(logger, request_dict, exception)
|
|
74
|
+
2025-02-01 12:34:56 - DEBUG - pyvesync -
|
|
75
|
+
Error in API CALL to endpoint: /api/v1/resource
|
|
76
|
+
Response Status: 500
|
|
77
|
+
Exception: <Exception>
|
|
78
|
+
Method: POST
|
|
79
|
+
Request Headers:
|
|
80
|
+
{
|
|
81
|
+
REQUEST HEADERS
|
|
82
|
+
}
|
|
83
|
+
Request Body:
|
|
84
|
+
{
|
|
85
|
+
"key": "value"
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
debug = False
|
|
90
|
+
"""Class attribute to enable or disable debug logging -
|
|
91
|
+
prints request and response content for errors only."""
|
|
92
|
+
shouldredact = True
|
|
93
|
+
"""Class attribute to determine if sensitive information should be redacted."""
|
|
94
|
+
verbose = False
|
|
95
|
+
"""Class attribute to print all request & response content."""
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def redactor(cls, stringvalue: str) -> str:
|
|
99
|
+
"""Redact sensitive strings from debug output.
|
|
100
|
+
|
|
101
|
+
This method searches for specific sensitive keys in the input string and replaces
|
|
102
|
+
their values with '##_REDACTED_##'. The keys that are redacted include:
|
|
103
|
+
|
|
104
|
+
- token
|
|
105
|
+
- authorizeCode
|
|
106
|
+
- password
|
|
107
|
+
- email
|
|
108
|
+
- tk
|
|
109
|
+
- accountId
|
|
110
|
+
- authKey
|
|
111
|
+
- uuid
|
|
112
|
+
- cid
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
stringvalue (str): The input string potentially containing
|
|
116
|
+
sensitive information.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
str: The redacted string with sensitive information replaced
|
|
120
|
+
by '##_REDACTED_##'.
|
|
121
|
+
"""
|
|
122
|
+
if cls.shouldredact:
|
|
123
|
+
stringvalue = re.sub(
|
|
124
|
+
(
|
|
125
|
+
r'(?i)'
|
|
126
|
+
r'((?<=token":\s")|'
|
|
127
|
+
r'(?<=password":\s")|'
|
|
128
|
+
r'(?<=email":\s")|'
|
|
129
|
+
r'(?<=tk":\s")|'
|
|
130
|
+
r'(?<=accountId":\s")|'
|
|
131
|
+
r'(?<=accountID":\s")|'
|
|
132
|
+
r'(?<=authKey":\s")|'
|
|
133
|
+
r'(?<=uuid":\s")|'
|
|
134
|
+
r'(?<=cid":\s")|'
|
|
135
|
+
r'(?<=token\s)|'
|
|
136
|
+
r'(?<=authorizeCode\s)|'
|
|
137
|
+
r'(?<=account_id\s))'
|
|
138
|
+
r'[^"\s]+'
|
|
139
|
+
),
|
|
140
|
+
'##_REDACTED_##',
|
|
141
|
+
stringvalue,
|
|
142
|
+
)
|
|
143
|
+
return stringvalue
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def is_json(data: str | bytes | None) -> bool:
|
|
147
|
+
"""Check if the data is JSON formatted."""
|
|
148
|
+
if data is None:
|
|
149
|
+
return False
|
|
150
|
+
if isinstance(data, str):
|
|
151
|
+
data = data.encode('utf-8')
|
|
152
|
+
try:
|
|
153
|
+
orjson.loads(data)
|
|
154
|
+
except orjson.JSONDecodeError:
|
|
155
|
+
return False
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def api_printer(cls, api: Mapping | bytes | str | None) -> str | None:
|
|
160
|
+
"""Print the API dictionary in a readable format."""
|
|
161
|
+
if api is None or len(api) == 0:
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
if isinstance(api, bytes):
|
|
165
|
+
api_dict = orjson.loads(api)
|
|
166
|
+
elif isinstance(api, str):
|
|
167
|
+
api_dict = orjson.loads(api.encode('utf-8'))
|
|
168
|
+
elif isinstance(api, (dict, CIMultiDictProxy)):
|
|
169
|
+
api_dict = dict(api)
|
|
170
|
+
else:
|
|
171
|
+
return None
|
|
172
|
+
dump = orjson.dumps(
|
|
173
|
+
dict(api_dict), option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
|
|
174
|
+
)
|
|
175
|
+
return cls.redactor(dump.decode('utf-8'))
|
|
176
|
+
|
|
177
|
+
except (orjson.JSONDecodeError, orjson.JSONEncodeError):
|
|
178
|
+
if isinstance(api, bytes):
|
|
179
|
+
return api.decode('utf-8')
|
|
180
|
+
return str(api)
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def set_log_level(level: str | int = logging.WARNING) -> None:
|
|
184
|
+
"""Set log level for logger to INFO."""
|
|
185
|
+
# Test default log levels if int or str
|
|
186
|
+
logging.getLogger().setLevel(level)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def configure_logger(
|
|
190
|
+
level: str | int = logging.INFO,
|
|
191
|
+
file_name: str | Path | None = None,
|
|
192
|
+
std_out: bool = True,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Configure pyvesync library logger with a specific log level.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
level (str | int): The log level to set the logger to, can be
|
|
198
|
+
in form of enum `logging.DEBUG` or string `DEBUG`.
|
|
199
|
+
file_name (str | None): The name of the file to log to. If None,
|
|
200
|
+
logs will only be printed to the console.
|
|
201
|
+
std_out (bool): If True, logs will be printed to standard output.
|
|
202
|
+
|
|
203
|
+
Note:
|
|
204
|
+
This method configures the pyvesync base logger and prevents
|
|
205
|
+
propagation of log messages to the root logger to avoid duplicate
|
|
206
|
+
messages.
|
|
207
|
+
"""
|
|
208
|
+
if level in (logging.DEBUG, 'DEBUG'):
|
|
209
|
+
LibraryLogger.debug = True
|
|
210
|
+
root_logger = logging.getLogger()
|
|
211
|
+
if root_logger.handlers:
|
|
212
|
+
for handler in root_logger.handlers:
|
|
213
|
+
root_logger.removeHandler(handler)
|
|
214
|
+
|
|
215
|
+
formatter = logging.Formatter(
|
|
216
|
+
fmt='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
|
217
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
218
|
+
)
|
|
219
|
+
if std_out is True:
|
|
220
|
+
str_handler = logging.StreamHandler()
|
|
221
|
+
str_handler.setFormatter(formatter)
|
|
222
|
+
root_logger.addHandler(str_handler)
|
|
223
|
+
if isinstance(file_name, str):
|
|
224
|
+
file_name_path = Path(file_name).resolve()
|
|
225
|
+
elif isinstance(file_name, Path):
|
|
226
|
+
file_name_path = file_name.resolve()
|
|
227
|
+
else:
|
|
228
|
+
file_name_path = None
|
|
229
|
+
if file_name_path:
|
|
230
|
+
file_handler = logging.FileHandler(file_name_path)
|
|
231
|
+
file_handler.setFormatter(formatter)
|
|
232
|
+
root_logger.addHandler(file_handler)
|
|
233
|
+
for log_name, logger in root_logger.manager.loggerDict.items():
|
|
234
|
+
if isinstance(logger, logging.Logger) and log_name.startswith('pyvesync'):
|
|
235
|
+
logger.setLevel(level)
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def log_mashumaro_response_error(
|
|
239
|
+
cls,
|
|
240
|
+
logger: logging.Logger,
|
|
241
|
+
method_name: str,
|
|
242
|
+
resp_dict: dict,
|
|
243
|
+
exc: MissingField | InvalidFieldValue | UnserializableField,
|
|
244
|
+
device: VeSyncBaseDevice | None = None,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Log an error parsing API response.
|
|
247
|
+
|
|
248
|
+
Use this log message to indicate that the API response
|
|
249
|
+
is not in the expected format.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
logger (logging.Logger): module logger
|
|
253
|
+
method_name (str): device name
|
|
254
|
+
resp_dict (dict): API response dictionary
|
|
255
|
+
exc (MissingField | InvalidFieldValue | UnserializableField): mashumaro
|
|
256
|
+
exception caught
|
|
257
|
+
device (VeSyncBaseDevice | None): device if a device method was called
|
|
258
|
+
"""
|
|
259
|
+
if device is not None:
|
|
260
|
+
msg = (
|
|
261
|
+
f'Error parsing {device.product_type} {device.device_name} {method_name} '
|
|
262
|
+
f'response with data model {exc.holder_class_name}'
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
msg = (
|
|
266
|
+
f'Error parsing {method_name} response with '
|
|
267
|
+
f'data model {exc.holder_class_name}'
|
|
268
|
+
)
|
|
269
|
+
if isinstance(exc, MissingField):
|
|
270
|
+
msg += f'Missing field: {exc.field_name} of type {exc.field_type_name}'
|
|
271
|
+
elif isinstance(exc, InvalidFieldValue):
|
|
272
|
+
msg += f'Invalid field value: {exc.field_name} of type {exc.field_type_name}'
|
|
273
|
+
elif isinstance(exc, UnserializableField):
|
|
274
|
+
msg += f'Unserializable field: {exc.field_name} of type {exc.field_type_name}'
|
|
275
|
+
msg += (
|
|
276
|
+
'\n\n Please report this issue tohttps://github.com/webdjoe/pyvesync/issues'
|
|
277
|
+
)
|
|
278
|
+
logger.warning(msg)
|
|
279
|
+
if not cls.debug:
|
|
280
|
+
return
|
|
281
|
+
msg = ''
|
|
282
|
+
if is_dataclass(exc.holder_class):
|
|
283
|
+
holder = exc.holder_class
|
|
284
|
+
field_tuple = fields(holder)
|
|
285
|
+
resp_f = sorted(resp_dict.keys())
|
|
286
|
+
field_f = sorted([f.name for f in field_tuple])
|
|
287
|
+
dif = set(field_f) - set(resp_f)
|
|
288
|
+
if dif:
|
|
289
|
+
msg += '\n\n-------------------------------------'
|
|
290
|
+
msg += '\n Expected Fields:'
|
|
291
|
+
msg += f'\n({", ".join(field_f)})'
|
|
292
|
+
msg += '\n Response Fields:'
|
|
293
|
+
msg += f'\n({", ".join(resp_f)})'
|
|
294
|
+
msg += '\n Missing Fields:'
|
|
295
|
+
msg += f'\n({", ".join(dif)})'
|
|
296
|
+
|
|
297
|
+
msg += '\n\n Full Response:'
|
|
298
|
+
msg += f'\n{orjson.dumps(resp_dict, option=orjson.OPT_INDENT_2).decode("utf-8")}'
|
|
299
|
+
if cls.verbose:
|
|
300
|
+
msg += '\n\n---------------------------------'
|
|
301
|
+
msg += '\n\n Exception:'
|
|
302
|
+
msg += f'\n{exc.__traceback__}'
|
|
303
|
+
logger.debug(msg)
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def log_vs_api_response_error(
|
|
307
|
+
cls,
|
|
308
|
+
logger: logging.Logger,
|
|
309
|
+
method_name: str,
|
|
310
|
+
msg: str | None = None,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Log an error parsing API response.
|
|
313
|
+
|
|
314
|
+
Use this log message to indicate that the API response
|
|
315
|
+
is not in the expected format.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
logger (logging.Logger): module logger
|
|
319
|
+
method_name (str): device name
|
|
320
|
+
msg (str | None, optional): optional description of error
|
|
321
|
+
"""
|
|
322
|
+
logger.debug(
|
|
323
|
+
'%s API returned an unexpected response format: %s',
|
|
324
|
+
method_name,
|
|
325
|
+
msg if msg is not None else '',
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def log_device_api_response_error(
|
|
330
|
+
cls,
|
|
331
|
+
logger: logging.Logger,
|
|
332
|
+
device_name: str,
|
|
333
|
+
device_type: str,
|
|
334
|
+
method: str,
|
|
335
|
+
msg: str | None = None,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Log an error parsing API response.
|
|
338
|
+
|
|
339
|
+
Use this log message to indicate that the API response
|
|
340
|
+
is not in the expected format.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
logger (logging.Logger): module logger
|
|
344
|
+
device_name (str): device name
|
|
345
|
+
device_type (str): device type
|
|
346
|
+
method (str): method that caused the error
|
|
347
|
+
msg (str | None, optional): optional description of error
|
|
348
|
+
"""
|
|
349
|
+
logger.debug(
|
|
350
|
+
'%s for %s API returned an unexpected response format in %s: %s',
|
|
351
|
+
device_name,
|
|
352
|
+
device_type,
|
|
353
|
+
method,
|
|
354
|
+
msg if msg is not None else '',
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def log_device_return_code(
|
|
359
|
+
cls,
|
|
360
|
+
logger: logging.Logger,
|
|
361
|
+
method: str,
|
|
362
|
+
device_name: str,
|
|
363
|
+
device_type: str,
|
|
364
|
+
code: int,
|
|
365
|
+
message: str = '',
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Log return code from device API call.
|
|
368
|
+
|
|
369
|
+
When API responds with JSON, if the code key is not 0,
|
|
370
|
+
it indicates an error has occured.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
logger (logging.Logger): module logger instance
|
|
374
|
+
method (str): method that caused the error
|
|
375
|
+
device_name (str): device name
|
|
376
|
+
device_type (str): device type
|
|
377
|
+
code (int): api response code
|
|
378
|
+
message (str): api response message
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
code_str = str(code)
|
|
382
|
+
except (TypeError, ValueError):
|
|
383
|
+
code_str = 'UNKNOWN'
|
|
384
|
+
logger.debug(
|
|
385
|
+
'%s for %s API from %s returned code: %s, message: %s',
|
|
386
|
+
device_name,
|
|
387
|
+
device_type,
|
|
388
|
+
method,
|
|
389
|
+
code_str,
|
|
390
|
+
message,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
@classmethod
|
|
394
|
+
def log_api_call(
|
|
395
|
+
cls,
|
|
396
|
+
logger: logging.Logger,
|
|
397
|
+
response: ClientResponse,
|
|
398
|
+
response_body: bytes | None = None,
|
|
399
|
+
request_body: str | dict | None = None,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Log API calls in debug mode.
|
|
402
|
+
|
|
403
|
+
Logs an API call with a specific format that includes the endpoint,
|
|
404
|
+
JSON-formatted headers, request body (if any) and response body.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
logger (logging.Logger): The logger instance to use.
|
|
408
|
+
response (aiohttp.ClientResponse): Requests response object from the API call.
|
|
409
|
+
response_body (bytes, optional): The response body to log.
|
|
410
|
+
request_body (dict | str, optional): The request body to log.
|
|
411
|
+
|
|
412
|
+
Notes:
|
|
413
|
+
This is a method used for the logging of API calls when the debug
|
|
414
|
+
flag is enabled. The method logs the endpoint, method, request headers,
|
|
415
|
+
request body (if any), response headers, and response body (if any).
|
|
416
|
+
"""
|
|
417
|
+
if cls.verbose is False:
|
|
418
|
+
return
|
|
419
|
+
# Build the log message parts.
|
|
420
|
+
parts = ['========API CALL========']
|
|
421
|
+
endpoint = response.url.path
|
|
422
|
+
parts.append(f'API CALL to endpoint: {endpoint}')
|
|
423
|
+
parts.append(f'Response Status: {response.status}')
|
|
424
|
+
parts.append(f'Method: {response.method}')
|
|
425
|
+
|
|
426
|
+
request_headers = cls.api_printer(response.request_info.headers)
|
|
427
|
+
if request_headers:
|
|
428
|
+
parts.append(f'Request Headers: {os.linesep} {request_headers}')
|
|
429
|
+
|
|
430
|
+
if request_body is not None:
|
|
431
|
+
request_body = cls.api_printer(request_body)
|
|
432
|
+
parts.append(f'Request Body: {os.linesep} {request_body}')
|
|
433
|
+
|
|
434
|
+
response_headers = cls.api_printer(response.headers)
|
|
435
|
+
if response_headers:
|
|
436
|
+
parts.append(f'Response Headers: {os.linesep} {response_headers}')
|
|
437
|
+
|
|
438
|
+
if cls.is_json(response_body):
|
|
439
|
+
response_str = cls.api_printer(response_body)
|
|
440
|
+
parts.append(f'Response Body: {os.linesep} {response_str}')
|
|
441
|
+
elif isinstance(response_body, bytes):
|
|
442
|
+
response_str = response_body.decode('utf-8')
|
|
443
|
+
parts.append(f'Response Body: {os.linesep} {response_str}')
|
|
444
|
+
|
|
445
|
+
full_message = os.linesep.join(parts)
|
|
446
|
+
logger.debug(full_message)
|
|
447
|
+
|
|
448
|
+
@classmethod
|
|
449
|
+
def log_api_status_error(
|
|
450
|
+
cls,
|
|
451
|
+
logger: logging.Logger,
|
|
452
|
+
*,
|
|
453
|
+
request_body: dict | None,
|
|
454
|
+
response: ClientResponse,
|
|
455
|
+
response_bytes: bytes | None,
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Log API exceptions in debug mode.
|
|
458
|
+
|
|
459
|
+
Logs an API call with a specific format that includes the endpoint,
|
|
460
|
+
JSON-formatted headers, request body (if any) and response body.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
logger (logging.Logger): The logger instance to use.
|
|
464
|
+
request_body (dict | None): KW only, The request body to log.
|
|
465
|
+
response (aiohttp.ClientResponse): KW only, dictionary
|
|
466
|
+
containing the request information.
|
|
467
|
+
response_bytes (bytes | None): KW only, The response body to log.
|
|
468
|
+
"""
|
|
469
|
+
if cls.debug is False:
|
|
470
|
+
return
|
|
471
|
+
# Build the log message parts.
|
|
472
|
+
parts = [f'Error in API CALL to endpoint: {response.url.path}']
|
|
473
|
+
parts.append(f'Response Status: {response.status}')
|
|
474
|
+
req_headers = cls.api_printer(response.request_info.headers)
|
|
475
|
+
if req_headers is not None:
|
|
476
|
+
parts.append(f'Request Headers: {os.linesep} {req_headers}')
|
|
477
|
+
|
|
478
|
+
req_body = cls.api_printer(request_body)
|
|
479
|
+
if req_body is not None:
|
|
480
|
+
parts.append(f'Request Body: {os.linesep} {req_body}')
|
|
481
|
+
|
|
482
|
+
resp_headers = cls.api_printer(response.headers)
|
|
483
|
+
if resp_headers is not None:
|
|
484
|
+
parts.append(f'Response Headers: {os.linesep} {resp_headers}')
|
|
485
|
+
|
|
486
|
+
resp_body = cls.api_printer(response_bytes)
|
|
487
|
+
if resp_body is not None:
|
|
488
|
+
parts.append(f'Request Body: {os.linesep} {request_body}')
|
|
489
|
+
|
|
490
|
+
full_message = os.linesep.join(parts)
|
|
491
|
+
logger.debug(full_message)
|
|
492
|
+
|
|
493
|
+
@classmethod
|
|
494
|
+
def log_api_exception(
|
|
495
|
+
cls,
|
|
496
|
+
logger: logging.Logger,
|
|
497
|
+
*,
|
|
498
|
+
exception: ClientResponseError,
|
|
499
|
+
request_body: dict | None,
|
|
500
|
+
) -> None:
|
|
501
|
+
"""Log API exceptions in debug mode.
|
|
502
|
+
|
|
503
|
+
Logs an API call with a specific format that includes the endpoint,
|
|
504
|
+
JSON-formatted headers, request body (if any) and response body.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
logger (logging.Logger): The logger instance to use.
|
|
508
|
+
exception (ClientResponseError): KW only, The request body to log.
|
|
509
|
+
request_body (dict | None): KW only, The request body.
|
|
510
|
+
"""
|
|
511
|
+
if cls.debug is False:
|
|
512
|
+
return
|
|
513
|
+
# Build the log message parts.
|
|
514
|
+
parts = [f'Error in API CALL to endpoint: {exception.request_info.url.path}']
|
|
515
|
+
parts.append(f'Exception Raised: {exception}')
|
|
516
|
+
req_headers = cls.api_printer(exception.request_info.headers)
|
|
517
|
+
if req_headers is not None:
|
|
518
|
+
parts.append(f'Request Headers: {os.linesep} {req_headers}')
|
|
519
|
+
|
|
520
|
+
req_body = cls.api_printer(request_body)
|
|
521
|
+
if req_body is not None:
|
|
522
|
+
parts.append(f'Request Body: {os.linesep} {req_body}')
|
|
523
|
+
|
|
524
|
+
full_message = os.linesep.join(parts)
|
|
525
|
+
logger.debug(full_message)
|