hn-deepocr-client 0.1.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.
- hn_deepocr_client/__init__.py +26 -0
- hn_deepocr_client/auth.py +58 -0
- hn_deepocr_client/config.py +35 -0
- hn_deepocr_client/exceptions.py +31 -0
- hn_deepocr_client/http_client.py +295 -0
- hn_deepocr_client/logging_config.py +56 -0
- hn_deepocr_client/parser.py +636 -0
- hn_deepocr_client/settings.py +45 -0
- hn_deepocr_client-0.1.0.dist-info/METADATA +267 -0
- hn_deepocr_client-0.1.0.dist-info/RECORD +12 -0
- hn_deepocr_client-0.1.0.dist-info/WHEEL +4 -0
- hn_deepocr_client-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from hn_deepocr_client.auth import create_token
|
|
2
|
+
from hn_deepocr_client.config import NetworkConfig
|
|
3
|
+
from hn_deepocr_client.exceptions import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ConfigurationError,
|
|
6
|
+
HNDeepOCRError,
|
|
7
|
+
ParseError,
|
|
8
|
+
UploadError,
|
|
9
|
+
)
|
|
10
|
+
from hn_deepocr_client.parser import ParserPDF
|
|
11
|
+
from hn_deepocr_client.settings import Settings, load_config
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Settings",
|
|
15
|
+
"load_config",
|
|
16
|
+
"NetworkConfig",
|
|
17
|
+
"ParserPDF",
|
|
18
|
+
"create_token",
|
|
19
|
+
"HNDeepOCRError",
|
|
20
|
+
"AuthenticationError",
|
|
21
|
+
"UploadError",
|
|
22
|
+
"ParseError",
|
|
23
|
+
"ConfigurationError",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import urllib.parse as uri_utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sign(sign_key: str, base_64_parameters: str, uri, request_body: str = "") -> str:
|
|
9
|
+
"""签名生成
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
sign_key: 签名密钥
|
|
13
|
+
base_64_parameters: Base64 编码的参数
|
|
14
|
+
uri: 请求 URI
|
|
15
|
+
request_body: 请求体,默认为空字符串
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
签名字符串(大写)
|
|
19
|
+
"""
|
|
20
|
+
data = base_64_parameters + get_relative_url(uri) + request_body
|
|
21
|
+
md5 = hashlib.md5((data + sign_key).encode("utf-8"))
|
|
22
|
+
return md5.hexdigest().upper()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_relative_url(uri) -> str:
|
|
26
|
+
"""获取相对 URL
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
uri: 解析后的 URI 对象
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
相对路径(包含查询参数,如果存在)
|
|
33
|
+
"""
|
|
34
|
+
if uri.query is None or len(uri.query) == 0:
|
|
35
|
+
return uri.path
|
|
36
|
+
return uri.path + "?" + uri.query
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_token(ak: str, sk: str, url: str = None, request_body: str = "") -> str:
|
|
40
|
+
"""创建认证 Token
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
ak: Access Key
|
|
44
|
+
sk: Secret Key
|
|
45
|
+
url: 完整请求 URL
|
|
46
|
+
request_body: 请求体,默认为空字符串
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
认证 Token,格式为:base64_parameters.signature
|
|
50
|
+
"""
|
|
51
|
+
uri = uri_utils.urlsplit(url, allow_fragments=False)
|
|
52
|
+
current_time = round(time.time() * 1000)
|
|
53
|
+
parameters = json.dumps(
|
|
54
|
+
{"alg": "MD5", "appKey": ak, "timestamp": str(current_time)}, separators=(",", ":")
|
|
55
|
+
)
|
|
56
|
+
base64_parameters = str(base64.b64encode(bytes(parameters, "utf-8")), "utf-8")
|
|
57
|
+
signature = sign(sk, base64_parameters, uri, request_body)
|
|
58
|
+
return base64_parameters + "." + signature
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Configuration management for hn-deepocr-client."""
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NetworkConfig(BaseSettings):
|
|
7
|
+
"""Network configuration for HTTP client."""
|
|
8
|
+
|
|
9
|
+
# Timeout configurations (seconds)
|
|
10
|
+
timeout_api: int = 30
|
|
11
|
+
timeout_upload: int = 300
|
|
12
|
+
timeout_status: int = 10
|
|
13
|
+
timeout_merge: int = 60
|
|
14
|
+
|
|
15
|
+
# Retry configurations
|
|
16
|
+
max_retries_default: int = 3
|
|
17
|
+
max_retries_upload: int = 5
|
|
18
|
+
base_delay: float = 1.0
|
|
19
|
+
|
|
20
|
+
# Connection pool configurations
|
|
21
|
+
connection_pool_size: int = 10
|
|
22
|
+
connection_maxsize: int = 20
|
|
23
|
+
|
|
24
|
+
# File partition configuration (bytes)
|
|
25
|
+
part_size: int = 10 * 1024 * 1024 # 10MB default
|
|
26
|
+
|
|
27
|
+
# SSL verification
|
|
28
|
+
verify_ssl: bool = True
|
|
29
|
+
|
|
30
|
+
model_config = SettingsConfigDict(
|
|
31
|
+
env_file=".env",
|
|
32
|
+
env_file_encoding="utf-8",
|
|
33
|
+
env_prefix="HN_DEEP_OCR_",
|
|
34
|
+
extra="ignore",
|
|
35
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""自定义异常类"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HNDeepOCRError(Exception):
|
|
5
|
+
"""基础异常类"""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthenticationError(HNDeepOCRError):
|
|
11
|
+
"""认证相关异常"""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UploadError(HNDeepOCRError):
|
|
17
|
+
"""文件上传相关异常"""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ParseError(HNDeepOCRError):
|
|
23
|
+
"""文件解析相关异常"""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigurationError(HNDeepOCRError):
|
|
29
|
+
"""配置相关异常"""
|
|
30
|
+
|
|
31
|
+
pass
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""HTTP client for hn-deepocr-client with connection pooling and retry logic."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from requests.adapters import HTTPAdapter
|
|
11
|
+
from requests.exceptions import ConnectionError, HTTPError, Timeout, TooManyRedirects
|
|
12
|
+
|
|
13
|
+
from hn_deepocr_client.config import NetworkConfig
|
|
14
|
+
from hn_deepocr_client.exceptions import AuthenticationError, ParseError, UploadError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HTTPClient:
|
|
18
|
+
"""HTTP client with connection pooling, retry logic, and standardized error handling."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: NetworkConfig, session: requests.Session | None = None):
|
|
21
|
+
"""Initialize HTTP client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config: Network configuration
|
|
25
|
+
session: Optional existing session to use
|
|
26
|
+
"""
|
|
27
|
+
self.config = config
|
|
28
|
+
self._logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
29
|
+
|
|
30
|
+
self._session = session or self._create_session()
|
|
31
|
+
self._owns_session = session is None
|
|
32
|
+
|
|
33
|
+
def _create_session(self) -> requests.Session:
|
|
34
|
+
"""Create and configure a requests session with connection pooling."""
|
|
35
|
+
session = requests.Session()
|
|
36
|
+
|
|
37
|
+
adapter = HTTPAdapter(
|
|
38
|
+
pool_connections=self.config.connection_pool_size,
|
|
39
|
+
pool_maxsize=self.config.connection_maxsize,
|
|
40
|
+
max_retries=0,
|
|
41
|
+
pool_block=False,
|
|
42
|
+
)
|
|
43
|
+
session.mount("http://", adapter)
|
|
44
|
+
session.mount("https://", adapter)
|
|
45
|
+
|
|
46
|
+
session.headers.update(
|
|
47
|
+
{
|
|
48
|
+
"User-Agent": "hn-deepocr-client/1.0",
|
|
49
|
+
"Accept": "application/json",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return session
|
|
54
|
+
|
|
55
|
+
def __enter__(self):
|
|
56
|
+
"""Context manager entry."""
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
60
|
+
"""Context manager exit - close session if we own it."""
|
|
61
|
+
if self._owns_session:
|
|
62
|
+
self._session.close()
|
|
63
|
+
|
|
64
|
+
def close(self):
|
|
65
|
+
"""Explicitly close the session."""
|
|
66
|
+
if self._owns_session:
|
|
67
|
+
self._session.close()
|
|
68
|
+
|
|
69
|
+
def _exponential_backoff_delay(self, attempt: int) -> float:
|
|
70
|
+
"""Calculate exponential backoff delay with jitter.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
attempt: Current attempt number (1-based)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Delay in seconds, guaranteed non-negative
|
|
77
|
+
"""
|
|
78
|
+
delay = min(self.config.base_delay * (2 ** (attempt - 1)), 60.0)
|
|
79
|
+
jitter = random.uniform(-0.25, 0.25)
|
|
80
|
+
return max(0.0, delay * (1 + jitter))
|
|
81
|
+
|
|
82
|
+
def _is_retryable_error(self, error: Exception) -> bool:
|
|
83
|
+
"""Determine if an error is retryable."""
|
|
84
|
+
if isinstance(error, (ConnectionError, Timeout, TooManyRedirects)):
|
|
85
|
+
return True
|
|
86
|
+
if isinstance(error, HTTPError):
|
|
87
|
+
return 500 <= error.response.status_code < 600
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def _handle_api_error(self, response: requests.Response, operation: str) -> None:
|
|
91
|
+
"""Handle API-specific errors with proper categorization.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
AuthenticationError: 401/403 responses
|
|
95
|
+
UploadError: 4xx client errors (except 401/403/404)
|
|
96
|
+
ParseError: 5xx server errors and other HTTP errors
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
except HTTPError as e:
|
|
101
|
+
status_code = response.status_code
|
|
102
|
+
if status_code == 401:
|
|
103
|
+
raise AuthenticationError(f"Authentication failed for {operation}: {e}") from e
|
|
104
|
+
elif status_code == 403:
|
|
105
|
+
raise AuthenticationError(f"Access denied for {operation}: {e}") from e
|
|
106
|
+
elif status_code == 404:
|
|
107
|
+
raise UploadError(f"Endpoint not found for {operation}: {e}") from e
|
|
108
|
+
elif 400 <= status_code < 500:
|
|
109
|
+
raise UploadError(f"Client error {status_code} for {operation}: {e}") from e
|
|
110
|
+
elif 500 <= status_code < 600:
|
|
111
|
+
raise ParseError(f"Server error {status_code} for {operation}: {e}") from e
|
|
112
|
+
else:
|
|
113
|
+
raise ParseError(f"HTTP error {status_code} for {operation}: {e}") from e
|
|
114
|
+
|
|
115
|
+
def _retry_on_failure(
|
|
116
|
+
self,
|
|
117
|
+
func,
|
|
118
|
+
max_retries: int,
|
|
119
|
+
retryable_errors: set[type[BaseException]] | None = None,
|
|
120
|
+
operation_name: str = "",
|
|
121
|
+
):
|
|
122
|
+
"""Generic retry mechanism with exponential backoff.
|
|
123
|
+
|
|
124
|
+
Only retries on network-level errors (ConnectionError, Timeout) and
|
|
125
|
+
5xx server errors. 4xx client errors (via AuthenticationError, UploadError)
|
|
126
|
+
are never retried.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
func: Callable to retry
|
|
130
|
+
max_retries: Maximum number of attempts (must be >= 1)
|
|
131
|
+
retryable_errors: Set of retryable exception types
|
|
132
|
+
operation_name: Name for logging
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If max_retries < 1
|
|
136
|
+
The last exception if all retries exhausted
|
|
137
|
+
"""
|
|
138
|
+
if max_retries < 1:
|
|
139
|
+
raise ValueError(f"max_retries must be >= 1, got {max_retries}")
|
|
140
|
+
|
|
141
|
+
if retryable_errors is None:
|
|
142
|
+
retryable_errors = {ConnectionError, Timeout, ParseError, HTTPError}
|
|
143
|
+
|
|
144
|
+
last_exception: Exception | None = None
|
|
145
|
+
|
|
146
|
+
for attempt in range(1, max_retries + 1):
|
|
147
|
+
try:
|
|
148
|
+
result = func()
|
|
149
|
+
return result
|
|
150
|
+
except Exception as e:
|
|
151
|
+
last_exception = e
|
|
152
|
+
|
|
153
|
+
if not any(isinstance(e, error_type) for error_type in retryable_errors):
|
|
154
|
+
self._logger.warning(f"Non-retryable error in {operation_name}: {str(e)}")
|
|
155
|
+
raise
|
|
156
|
+
|
|
157
|
+
if attempt == max_retries:
|
|
158
|
+
self._logger.error(
|
|
159
|
+
f"Max retries exceeded for {operation_name}: {str(e)}",
|
|
160
|
+
exc_info=True,
|
|
161
|
+
)
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
delay = self._exponential_backoff_delay(attempt)
|
|
165
|
+
self._logger.warning(
|
|
166
|
+
f"Attempt {attempt}/{max_retries} failed for {operation_name}, "
|
|
167
|
+
f"retrying in {delay:.2f}s: {str(e)}"
|
|
168
|
+
)
|
|
169
|
+
time.sleep(delay)
|
|
170
|
+
|
|
171
|
+
raise last_exception
|
|
172
|
+
|
|
173
|
+
def make_request(
|
|
174
|
+
self,
|
|
175
|
+
method: str,
|
|
176
|
+
url: str,
|
|
177
|
+
timeout_key: str = "api",
|
|
178
|
+
**kwargs,
|
|
179
|
+
) -> requests.Response:
|
|
180
|
+
"""Make HTTP request using the configured session."""
|
|
181
|
+
timeout_map = {
|
|
182
|
+
"api": self.config.timeout_api,
|
|
183
|
+
"upload": self.config.timeout_upload,
|
|
184
|
+
"status": self.config.timeout_status,
|
|
185
|
+
"merge": self.config.timeout_merge,
|
|
186
|
+
}
|
|
187
|
+
timeout = timeout_map.get(timeout_key, self.config.timeout_api)
|
|
188
|
+
|
|
189
|
+
if "timeout" not in kwargs:
|
|
190
|
+
kwargs["timeout"] = timeout
|
|
191
|
+
|
|
192
|
+
if "headers" not in kwargs:
|
|
193
|
+
kwargs["headers"] = {}
|
|
194
|
+
|
|
195
|
+
headers = dict(self._session.headers)
|
|
196
|
+
headers.update(kwargs["headers"])
|
|
197
|
+
kwargs["headers"] = headers
|
|
198
|
+
|
|
199
|
+
kwargs.setdefault("verify", self.config.verify_ssl)
|
|
200
|
+
|
|
201
|
+
self._logger.debug(f"Making {method} request to {url}")
|
|
202
|
+
return self._session.request(method, url, **kwargs)
|
|
203
|
+
|
|
204
|
+
def post_with_retry(
|
|
205
|
+
self,
|
|
206
|
+
url: str,
|
|
207
|
+
data: dict[str, Any] | str,
|
|
208
|
+
headers: dict[str, str] | None = None,
|
|
209
|
+
timeout_key: str = "api",
|
|
210
|
+
max_retries: int | None = None,
|
|
211
|
+
operation_name: str = "",
|
|
212
|
+
) -> requests.Response:
|
|
213
|
+
"""Make POST request with retry logic."""
|
|
214
|
+
if max_retries is None:
|
|
215
|
+
max_retries = self.config.max_retries_default
|
|
216
|
+
|
|
217
|
+
def _attempt():
|
|
218
|
+
if isinstance(data, dict):
|
|
219
|
+
json_data = json.dumps(data, ensure_ascii=False)
|
|
220
|
+
else:
|
|
221
|
+
json_data = str(data)
|
|
222
|
+
|
|
223
|
+
response = self.make_request(
|
|
224
|
+
"POST",
|
|
225
|
+
url=url,
|
|
226
|
+
data=json_data,
|
|
227
|
+
headers=headers or {},
|
|
228
|
+
timeout_key=timeout_key,
|
|
229
|
+
)
|
|
230
|
+
self._handle_api_error(response, operation_name)
|
|
231
|
+
return response
|
|
232
|
+
|
|
233
|
+
return self._retry_on_failure(
|
|
234
|
+
_attempt,
|
|
235
|
+
max_retries=max_retries,
|
|
236
|
+
operation_name=operation_name,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def get_with_retry(
|
|
240
|
+
self,
|
|
241
|
+
url: str,
|
|
242
|
+
headers: dict[str, str] | None = None,
|
|
243
|
+
timeout_key: str = "status",
|
|
244
|
+
max_retries: int | None = None,
|
|
245
|
+
operation_name: str = "",
|
|
246
|
+
) -> requests.Response:
|
|
247
|
+
"""Make GET request with retry logic."""
|
|
248
|
+
if max_retries is None:
|
|
249
|
+
max_retries = self.config.max_retries_default
|
|
250
|
+
|
|
251
|
+
def _attempt():
|
|
252
|
+
response = self.make_request(
|
|
253
|
+
"GET",
|
|
254
|
+
url=url,
|
|
255
|
+
headers=headers or {},
|
|
256
|
+
timeout_key=timeout_key,
|
|
257
|
+
)
|
|
258
|
+
self._handle_api_error(response, operation_name)
|
|
259
|
+
return response
|
|
260
|
+
|
|
261
|
+
return self._retry_on_failure(
|
|
262
|
+
_attempt,
|
|
263
|
+
max_retries=max_retries,
|
|
264
|
+
operation_name=operation_name,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def put_with_retry(
|
|
268
|
+
self,
|
|
269
|
+
url: str,
|
|
270
|
+
data: bytes,
|
|
271
|
+
headers: dict[str, str] | None = None,
|
|
272
|
+
timeout_key: str = "upload",
|
|
273
|
+
max_retries: int | None = None,
|
|
274
|
+
operation_name: str = "",
|
|
275
|
+
) -> requests.Response:
|
|
276
|
+
"""Make PUT request with retry logic."""
|
|
277
|
+
if max_retries is None:
|
|
278
|
+
max_retries = self.config.max_retries_upload
|
|
279
|
+
|
|
280
|
+
def _attempt():
|
|
281
|
+
response = self.make_request(
|
|
282
|
+
"PUT",
|
|
283
|
+
url=url,
|
|
284
|
+
data=data,
|
|
285
|
+
headers=headers or {},
|
|
286
|
+
timeout_key=timeout_key,
|
|
287
|
+
)
|
|
288
|
+
self._handle_api_error(response, operation_name)
|
|
289
|
+
return response
|
|
290
|
+
|
|
291
|
+
return self._retry_on_failure(
|
|
292
|
+
_attempt,
|
|
293
|
+
max_retries=max_retries,
|
|
294
|
+
operation_name=operation_name,
|
|
295
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from logging.handlers import RotatingFileHandler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def init_logger(
|
|
7
|
+
logger_name: str | None = None,
|
|
8
|
+
log_dir: str = "logs",
|
|
9
|
+
log_file: str = "pdf_parser.log",
|
|
10
|
+
log_level: int | None = None,
|
|
11
|
+
max_bytes: int = 10 * 1024 * 1024,
|
|
12
|
+
backup_count: int = 5,
|
|
13
|
+
):
|
|
14
|
+
"""初始化日志配置:同时输出到控制台和文件
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
logger_name: logger 名称,默认为 "hn_deepocr_client"
|
|
18
|
+
log_dir: 日志目录,默认为 "logs"
|
|
19
|
+
log_file: 日志文件名,默认为 "pdf_parser.log"
|
|
20
|
+
log_level: 日志级别,默认为 logging.INFO,支持环境变量 LOG_LEVEL
|
|
21
|
+
max_bytes: 单个日志文件最大字节数,默认为 10MB
|
|
22
|
+
backup_count: 保留的备份文件数量,默认为 5
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
配置好的 logger 实例
|
|
26
|
+
"""
|
|
27
|
+
if logger_name is None:
|
|
28
|
+
logger_name = "hn_deepocr_client"
|
|
29
|
+
|
|
30
|
+
effective_level = log_level
|
|
31
|
+
if effective_level is None:
|
|
32
|
+
log_level_str = os.environ.get("LOG_LEVEL", "INFO")
|
|
33
|
+
effective_level = getattr(logging, log_level_str.upper(), logging.INFO)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(logger_name)
|
|
36
|
+
if logger.handlers:
|
|
37
|
+
return logger
|
|
38
|
+
|
|
39
|
+
logger.setLevel(effective_level)
|
|
40
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
41
|
+
|
|
42
|
+
console_handler = logging.StreamHandler()
|
|
43
|
+
console_handler.setFormatter(formatter)
|
|
44
|
+
logger.addHandler(console_handler)
|
|
45
|
+
|
|
46
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
47
|
+
file_handler = RotatingFileHandler(
|
|
48
|
+
filename=os.path.join(log_dir, log_file),
|
|
49
|
+
maxBytes=max_bytes,
|
|
50
|
+
backupCount=backup_count,
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
53
|
+
file_handler.setFormatter(formatter)
|
|
54
|
+
logger.addHandler(file_handler)
|
|
55
|
+
|
|
56
|
+
return logger
|