vos-data-utils 0.0.2__py3-none-any.whl → 0.0.4__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.

Potentially problematic release.


This version of vos-data-utils might be problematic. Click here for more details.

vdutils/genpnu.py ADDED
@@ -0,0 +1,623 @@
1
+ import re
2
+ import pkg_resources
3
+ import pandas as pd
4
+ from typing import (
5
+ Any,
6
+ List,
7
+ Dict,
8
+ Tuple,
9
+ Optional
10
+ )
11
+ from dataclasses import dataclass
12
+ from collections import defaultdict
13
+ from vdutils.library.data import (
14
+ SGG_SPLIT_LIST,
15
+ LAST_NM_REFINE_MAP
16
+ )
17
+ from vdutils.library import Log
18
+ from vdutils.data import (
19
+ __sep__,
20
+ __index__,
21
+ __encoding__,
22
+ _get_folder_names
23
+ )
24
+
25
+
26
+ @dataclass
27
+ class GenPnu():
28
+
29
+
30
+ def __init__(
31
+ self,
32
+ base_dt: Optional[str] = None
33
+ ):
34
+
35
+ if base_dt is not None:
36
+ if not isinstance(base_dt, str):
37
+ raise TypeError("type of object('base_dt') must be string")
38
+
39
+ if not base_dt.isdigit():
40
+ raise ValueError("object('base_dt') should be a string consisting of numbers")
41
+
42
+ if len(base_dt) != 8:
43
+ raise ValueError("object('base_dt') should be a string consisting of exactly 8(YYYYMMDD) digits")
44
+ else: pass
45
+
46
+ self.sep = __sep__
47
+ self.index: bool = __index__
48
+ self.encoding: str = __encoding__
49
+ self.base_dt: Optional[str] = base_dt
50
+ self.bjd_current_df: pd.DataFrame() = None
51
+ self.bjd_current_nm_cd_dic = defaultdict(list)
52
+ self.bjd_dic: Dict[str, Dict[str, str]] = {}
53
+ self.bjd_nm_change_dic: Dict[str, str] = {
54
+ "시도명": "sido_nm",
55
+ "시군구명": "sgg_nm",
56
+ "읍면동명": "emd_nm",
57
+ "리명": "ri_nm",
58
+ }
59
+ self.logger = Log('GeneratePnu').stream_handler("INFO")
60
+ self._get_base_dt()
61
+ self._get_file_names()
62
+ self._prepare()
63
+ self.base_dt_print: str = f"{self.base_dt[:4]}-{self.base_dt[4:6]}-{self.base_dt[6:8]}"
64
+
65
+
66
+ def _find_latest_base_dt(
67
+ self,
68
+ base_dts: List[str]
69
+ ) -> str:
70
+
71
+ """
72
+ 입력된 날짜(YYYYMMDD)와 법정동 데이터 시점 리스트와 비교하여 입력된 날짜보다 과거 시점 중 최신 시점을 반환
73
+ """
74
+
75
+ for date in base_dts:
76
+ if date < self.base_dt:
77
+ return date
78
+
79
+ # 입력된 날짜보다 작은 날짜가 없을 경우
80
+ self.logger.info("입력된 날짜보다 이전 시점의 법정동 데이터가 존재하지 않습니다. 보유한 데이터중 최신 데이터를 적용합니다.")
81
+ return base_dts[0]
82
+
83
+
84
+ def _get_base_dt(self):
85
+
86
+ """
87
+ 입력된 날짜(YYYYMMDD)와 법정동 데이터 시점 리스트와 비교하여 입력된 날짜보다 과거 시점 중 최신 시점을 반환 \n
88
+ 입력된 날짜(YYYYMMDD)가 없으면 데이터 시점 리스트 중 최신 시점을 반환
89
+ """
90
+
91
+ base_dts = _get_folder_names(base_folder_path='vdutils/data/bjd')
92
+ base_dts = sorted(base_dts, reverse=True)
93
+ try:
94
+ if self.base_dt is None:
95
+ self.base_dt = base_dts[0]
96
+ else:
97
+ self.base_dt = self._find_latest_base_dt(base_dts=base_dts)
98
+ finally:
99
+ self.logger.info(f"적용 법정동 데이터 시점: {self.base_dt}")
100
+
101
+
102
+ def _get_file_names(self):
103
+ self.file_name_bjd_current = pkg_resources.resource_filename(
104
+ "vdutils",
105
+ f"data/bjd/{self.base_dt}/bjd_current.txt"
106
+ )
107
+
108
+
109
+ def _get_bjd_current_df(
110
+ self,
111
+ file_name_bjd_current,
112
+ input_encoding,
113
+ input_index,
114
+ input_sep
115
+ ):
116
+ try:
117
+ self.bjd_current_df: pd.DataFrame = pd.read_csv(
118
+ file_name_bjd_current,
119
+ sep=input_sep,
120
+ engine='python',
121
+ encoding=input_encoding,
122
+ dtype={
123
+ '과거법정동코드': str,
124
+ '법정동코드': str
125
+ }
126
+ )
127
+ except Exception as e:
128
+ self.logger.error(f"Failed to read {file_name_bjd_current}")
129
+ self.logger.error(e)
130
+
131
+
132
+ def _create_bjd_current_nm_cd_dic(self):
133
+ try:
134
+ for idx, row in self.bjd_current_df.iterrows():
135
+ bjd_nms = []
136
+ for bjd_nm in ["시도명", "시군구명", "읍면동명", "리명"]:
137
+ if not pd.isna(row[bjd_nm]):
138
+ bjd_nms.append(row[bjd_nm])
139
+ bjd_nms = " ".join(bjd_nms)
140
+ self.bjd_current_nm_cd_dic[bjd_nms].append(
141
+ {
142
+ "bjd_cd": row["법정동코드"],
143
+ "deleted_dt": None if pd.isna(row["삭제일자"]) else row["삭제일자"]
144
+ }
145
+ )
146
+ except Exception as e:
147
+ self.logger.error(f"Failed to create bjd_current_nm_cd_dic")
148
+ self.logger.error(e)
149
+
150
+
151
+ def _create_bjd_dic(self):
152
+ try:
153
+ for idx, row in self.bjd_current_df.iterrows():
154
+ bjd_cd: str = row["법정동코드"]
155
+ full_bjd_nm: str = row["법정동명"]
156
+ created_dt: str = row["생성일자"]
157
+ deleted_dt: str = row["삭제일자"]
158
+ bjd_datas: Dict[str, str] = {}
159
+ for bjd_nm in ["시도명", "시군구명", "읍면동명", "리명"]:
160
+ if not pd.isna(row[bjd_nm]):
161
+ bjd_datas[self.bjd_nm_change_dic[bjd_nm]] = row[bjd_nm]
162
+ else:
163
+ bjd_datas[self.bjd_nm_change_dic[bjd_nm]] = None
164
+ bjd_datas["full_bjd_nm"] = full_bjd_nm
165
+ bjd_datas["created_dt"] = created_dt
166
+ bjd_datas["deleted_dt"] = None if pd.isna(deleted_dt) else deleted_dt
167
+ self.bjd_dic[bjd_cd] = bjd_datas
168
+ except Exception as e:
169
+ self.logger.error(f"Failed to create bjd_dic")
170
+ self.logger.error(e)
171
+
172
+
173
+ def _prepare(self):
174
+ self._get_bjd_current_df(
175
+ file_name_bjd_current=self.file_name_bjd_current,
176
+ input_encoding=self.encoding,
177
+ input_index=self.index,
178
+ input_sep=self.sep,
179
+ )
180
+ self._create_bjd_current_nm_cd_dic()
181
+ self._create_bjd_dic()
182
+
183
+
184
+ def get_bjd_cd(
185
+ self,
186
+ bjd_nm: str,
187
+ ) -> Dict[str, Any]:
188
+
189
+ """
190
+ 입력된 문자열(한글 법정동명)의 법정동 코드를 반환
191
+
192
+ Args:
193
+ bjd_nm (str): The input should be a string consisting of Korean administrative district names.
194
+
195
+ Raises:
196
+ TypeError: If the 'bjd_nm' object is not of type string.
197
+ ValueError: If the 'bjd_nm' object is not consist of only Korean characters and numbers.
198
+
199
+ Returns:
200
+ Dict[str, Any]:
201
+ "error": bool,
202
+ "bjd_cd": Optional[str],
203
+ "deleted_dt": Optional[str],
204
+ "base_dt": str,
205
+ "msg": str
206
+ """
207
+
208
+ if not isinstance(bjd_nm, str):
209
+ raise TypeError("type of object('bjd_nm') must be string")
210
+
211
+ if not re.match("^[가-힣0-9 ]+$", bjd_nm):
212
+ raise ValueError("object('bjd_nm') should consist of only Korean characters and numbers")
213
+
214
+ try:
215
+ not_a_valid_district_response: Dict[str, Any] = {
216
+ "error": True,
217
+ "bjd_cd": None,
218
+ "deleted_dt": None,
219
+ "base_dt": self.base_dt_print,
220
+ "msg": f"'{bjd_nm}' is not a valid legal district name"
221
+ }
222
+ bjd_nm = " ".join(bjd_nm.split())
223
+
224
+ if bjd_nm in self.bjd_current_nm_cd_dic:
225
+ bjd_cd_list = self.bjd_current_nm_cd_dic[bjd_nm]
226
+ if len(bjd_cd_list) > 1:
227
+ bjd_cd = list(
228
+ filter(
229
+ lambda bjd_cd_data: not bjd_cd_data["deleted_dt"], bjd_cd_list
230
+ )
231
+ )[0]
232
+ else:
233
+ bjd_cd = bjd_cd_list[0]
234
+
235
+ return {
236
+ "error": False,
237
+ **bjd_cd,
238
+ "base_dt": self.base_dt_print,
239
+ "msg": ""
240
+ }
241
+
242
+ else:
243
+ if len(bjd_nm.split()) == 1:
244
+ return not_a_valid_district_response
245
+ else:
246
+ sgg = bjd_nm.split()[1]
247
+ if sgg in SGG_SPLIT_LIST:
248
+ sgg_split_nm = f"{sgg[:2]}시 {sgg[2:]}"
249
+ bjd_nm = bjd_nm.replace(sgg, sgg_split_nm)
250
+ return self.get_bjd_cd(bjd_nm)
251
+
252
+ last_nm = bjd_nm.split()[-1]
253
+ if last_nm in LAST_NM_REFINE_MAP:
254
+ bjd_nm = bjd_nm.replace(last_nm, LAST_NM_REFINE_MAP[last_nm])
255
+ return self.get_bjd_cd(bjd_nm)
256
+
257
+ return not_a_valid_district_response
258
+
259
+ except:
260
+ return not_a_valid_district_response
261
+
262
+
263
+ def get_bjd_data(
264
+ self,
265
+ bjd_cd: str
266
+ ) -> Dict[str, Any]:
267
+
268
+ """
269
+ 입력된 문자열(숫자 10자리의 법정동코드)의 법정동 데이터(각 단위 법정동명, 생성일자, 삭제일자)를 반환
270
+
271
+ Args:
272
+ bjd_cd (str): The Korean district code string consisting of exactly 10 digits.
273
+
274
+ Raises:
275
+ TypeError: If the 'bjd_cd' object is not of type string.
276
+ ValueError: If the 'bjd_cd' object does not consist of digits only.
277
+ ValueError: If the 'bjd_cd' object does not consist of exactly 10 digits.
278
+
279
+ Returns:
280
+ Dict[str, Any]: {
281
+ "error": bool,
282
+ "sido_nm": Optional[str],
283
+ "sgg_nm": Optional[str],
284
+ "emd_nm": Optional[str],
285
+ "ri_nm": Optional[str],
286
+ "full_bjd_nm": Optional[str],
287
+ "created_dt": Optional[str],
288
+ "deleted_dt": Optional[str],
289
+ "base_dt": str
290
+ }
291
+ """
292
+
293
+ if not isinstance(bjd_cd, str):
294
+ raise TypeError("type of object('bjd_cd') must be string")
295
+
296
+ if not bjd_cd.isdigit():
297
+ raise ValueError("object('bjd_cd') should be a string consisting of numbers")
298
+
299
+ if len(bjd_cd) != 10:
300
+ raise ValueError("object('bjd_cd') should be a string consisting of exactly 10 digits")
301
+
302
+ try:
303
+ not_a_valid_district_response: Dict[str, Any] = {
304
+ "error": True,
305
+ "sido_nm": None,
306
+ "sgg_nm": None,
307
+ "emd_nm": None,
308
+ "ri_nm": None,
309
+ "full_bjd_nm": None,
310
+ "created_dt": None,
311
+ "deleted_dt": None,
312
+ "base_dt": self.base_dt_print,
313
+ "msg": f"'{bjd_cd}' is not a valid legal district code"
314
+ }
315
+ if bjd_cd in self.bjd_dic:
316
+ return {"error": False, **self.bjd_dic[bjd_cd], "base_dt": self.base_dt_print, "msg": ""}
317
+ else:
318
+ return {"error": True, **not_a_valid_district_response}
319
+ except Exception as e:
320
+ return {"error": True, **not_a_valid_district_response.update({"msg": str(e)})}
321
+
322
+
323
+ @staticmethod
324
+ def _validate_jibun(
325
+ jibun: Optional[str]
326
+ ) -> bool:
327
+
328
+ """
329
+ 입력된 지번 문자열이 올바른 형식인지 정규식을 이용하여 검증하여 반환 \n
330
+ 단, 블록지번를 의미하는 음절이 포함되거나 '*' 가 포함될 경우 예외 적용하여 True 를 리턴
331
+
332
+ Args:
333
+ jibun (str): Validates the format of the given address.
334
+ The address should include '산' and only contain digits except for '산' and '-'.
335
+ The main and sub numbers should be separated by a hyphen, and both can have a maximum of 4 digits.
336
+ Examples:
337
+ With mountain and sub-number: 산 0000-0000
338
+ With mountain and no sub-number: 산 0000
339
+ Without mountain and with sub-number: 0000-0000
340
+ Without mountain and without sub-number: 0000
341
+
342
+ Raises:
343
+ ValueError: If the 'jibun' object is not of the specified format.
344
+
345
+ Returns:
346
+ bool
347
+ """
348
+
349
+ msg = """
350
+ Invalid 'jibun' format. Please follow the specified format.
351
+
352
+ The address should include '산' and only contain digits except for '산' and '-'.
353
+ The main and sub numbers should be separated by a hyphen, and both can have a maximum of 4 digits.
354
+ Examples:
355
+ With mountain and sub-number: 산 0000-0000
356
+ With mountain and no sub-number: 산 0000
357
+ Without mountain and with sub-number: 0000-0000
358
+ Without mountain and without sub-number: 0000
359
+ """
360
+
361
+ if pd.isna(jibun) \
362
+ or jibun == "" \
363
+ or jibun is None \
364
+ or jibun[0] in ["B", "가", "지"]:
365
+ return True
366
+ if "*" in jibun:
367
+ return True
368
+
369
+ jibun = jibun.replace(" ", "")
370
+ pattern = re.compile(r'^(산\s*)?\d{1,4}-\d{1,4}$|^(산\s*)?\d{1,4}$|^\d{1,4}-\d{1,4}$|^\d{1,4}$')
371
+
372
+ if not bool(pattern.match(jibun)):
373
+ raise ValueError(msg)
374
+
375
+ return True
376
+
377
+
378
+ @staticmethod
379
+ def _get_mountain_cd(
380
+ jibun: str
381
+ ) -> Tuple[str, str]:
382
+
383
+ """
384
+ 입력된 지번 문자열(지번 문자열 적합성 확인된 입력값)에서 '산' 여부 판단하여 산코드를 반환
385
+ """
386
+
387
+ if jibun[0] in ["산"]:
388
+ mountain_cd = "2"
389
+ jibun = jibun.replace("산", "")
390
+
391
+ else:
392
+ mountain_cd = "1"
393
+
394
+ return jibun, mountain_cd
395
+
396
+
397
+ @staticmethod
398
+ def _get_jibun_datas(
399
+ jibun: str
400
+ ) -> Tuple[str, str, str]:
401
+
402
+ """
403
+ 입력된 지번 문자열(지번 문자열 적합성 확인된 입력값)에서 본번과 부번을 분리하여 번, 지 코드를 반환
404
+ """
405
+
406
+ jibun_split = jibun.split("-")
407
+
408
+ if len(jibun_split) == 2:
409
+ bun, ji = [int(num) for num in jibun_split]
410
+ bunji_cd = "%04d%04d" % (bun, ji)
411
+ bun = str(bun)
412
+ ji = str(ji)
413
+
414
+ elif len(jibun_split) == 1:
415
+ bun = int(jibun)
416
+ bunji_cd = "%04d0000" % (bun)
417
+ bun = str(bun)
418
+ ji = "0"
419
+
420
+ else:
421
+ bunji_cd = "00000000"
422
+ bun, ji = "0", "0"
423
+
424
+ return bunji_cd, bun, ji
425
+
426
+
427
+ def generate_pnu(
428
+ self,
429
+ bjd_cd: str,
430
+ jibun: str # '산'을 포함한 지번
431
+ ) -> Dict[str, Any]:
432
+
433
+ """
434
+ 입력된 문자열(법정동 코드, 지번)을 필지관리번호(pnu)로 변환하여 반환
435
+
436
+ Args:
437
+ bjd_cd (str): The Korean district code string consisting of exactly 10 digits.
438
+ jibun (str): Validates the format of the given address.
439
+ The address should include '산' and only contain digits except for '산' and '-'.
440
+ The main and sub numbers should be separated by a hyphen, and both can have a maximum of 4 digits.
441
+ Examples:
442
+ With mountain and sub-number: 산 0000-0000
443
+ With mountain and no sub-number: 산 0000
444
+ Without mountain and with sub-number: 0000-0000
445
+ Without mountain and without sub-number: 0000
446
+
447
+ Raises:
448
+ TypeError: If the 'bjd_cd' object is not of type string.
449
+ TypeError: If the 'jibun' object is not of type string.
450
+ ValueError: If the 'bjd_cd' object does not consist of digits only.
451
+ ValueError: If the 'bjd_cd' object does not consist of exactly 10 digits.
452
+ ValueError: If the 'jibun' object is not of the specified format.
453
+
454
+ Returns:
455
+ Dict[str, Any]: {
456
+ "error": bool,
457
+ "pnu": str,
458
+ "bjd_cd": str,
459
+ "mountain_cd": str,
460
+ "bunji_cd": str,
461
+ "bjd_datas": Dict[str, Any],
462
+ "bun": str,
463
+ "ji": str,
464
+ "msg": str,
465
+ "base_dt": str
466
+ }
467
+ """
468
+
469
+ if not isinstance(bjd_cd, str):
470
+ raise TypeError("type of object('bjd_cd') must be string")
471
+
472
+ if not isinstance(jibun, str):
473
+ raise TypeError("type of object('jibun') must be string")
474
+
475
+ if not bjd_cd.isdigit():
476
+ raise ValueError("object('bjd_cd') should be a string consisting of numbers")
477
+
478
+ if len(bjd_cd) != 10:
479
+ raise ValueError("object('bjd_cd') should be a string consisting of exactly 10 digits")
480
+
481
+ if self._validate_jibun(jibun):
482
+ msg = ""
483
+ try:
484
+ bjd_datas = self.get_bjd_data(bjd_cd)
485
+
486
+ if pd.isna(jibun) \
487
+ or jibun == "" \
488
+ or jibun is None \
489
+ or jibun[0] in ["B", "가", "지"]:
490
+ error = True
491
+ mountain_cd = "1"
492
+ bunji_cd = "00000000"
493
+ bun, ji = "0", "0"
494
+ msg = "블록지번"
495
+
496
+ elif "*" in jibun:
497
+ error = True
498
+ mountain_cd = "1"
499
+ bunji_cd = "00000000"
500
+ bun, ji = "0", "0"
501
+ msg = "매칭필요"
502
+
503
+ else:
504
+ error = bjd_datas.get("error")
505
+ if error is True:
506
+ msg = bjd_datas.get("msg")
507
+ jibun = jibun.replace(" ", "")
508
+ jibun, mountain_cd = self._get_mountain_cd(jibun)
509
+ bunji_cd, bun, ji = self._get_jibun_datas(jibun)
510
+
511
+ except Exception as e:
512
+ error = True
513
+ mountain_cd = "1"
514
+ bunji_cd = "00000000"
515
+ bun, ji = "0", "0"
516
+ msg = str(e)
517
+
518
+ return {
519
+ "error": error,
520
+ "pnu": f"{bjd_cd}{mountain_cd}{bunji_cd}",
521
+ "bjd_cd": bjd_cd,
522
+ "mountain_cd": mountain_cd,
523
+ "bunji_cd": bunji_cd,
524
+ "bjd_datas": bjd_datas,
525
+ "bun": bun,
526
+ "ji": ji,
527
+ "msg": msg,
528
+ "base_dt": self.base_dt_print
529
+ }
530
+
531
+
532
+ def generate_pnu_from_bjd_nm(
533
+ self,
534
+ bjd_nm: str,
535
+ jibun: str # '산'을 포함한 지번
536
+ ) -> Dict[str, Any]:
537
+
538
+ """
539
+ 입력된 문자열(법정동 코드, 지번)을 필지관리번호(pnu)로 변환하여 반환
540
+
541
+ Args:
542
+ bjd_nm (str): The input should be a string consisting of Korean administrative district names.
543
+ jibun (str): Validates the format of the given address.
544
+ The address should include '산' and only contain digits except for '산' and '-'.
545
+ The main and sub numbers should be separated by a hyphen, and both can have a maximum of 4 digits.
546
+ Examples:
547
+ With mountain and sub-number: 산 0000-0000
548
+ With mountain and no sub-number: 산 0000
549
+ Without mountain and with sub-number: 0000-0000
550
+ Without mountain and without sub-number: 0000
551
+
552
+ Raises:
553
+ TypeError: If the 'bjd_nm' object is not of type string.
554
+ TypeError: If the 'jibun' object is not of type string.
555
+ ValueError: If the 'bjd_nm' object is not consist of only Korean characters and numbers.
556
+ ValueError: If the 'jibun' object is not of the specified format.
557
+
558
+ Returns:
559
+ Dict[str, Any]: {
560
+ "error": bool,
561
+ "pnu": str,
562
+ "bjd_cd": str,
563
+ "mountain_cd": str,
564
+ "bunji_cd": str,
565
+ "bjd_datas": Dict[str, Any],
566
+ "bun": str,
567
+ "ji": str,
568
+ "msg": str,
569
+ "base_dt": str
570
+ }
571
+ """
572
+
573
+ if not isinstance(bjd_nm, str):
574
+ raise TypeError("type of object('bjd_nm') must be string")
575
+
576
+ if not isinstance(jibun, str):
577
+ raise TypeError("type of object('jibun') must be string")
578
+
579
+ if not re.match("^[가-힣0-9 ]+$", bjd_nm):
580
+ raise ValueError("object('bjd_nm') should consist of only Korean characters and numbers")
581
+
582
+ if self._validate_jibun(jibun):
583
+ res = self.get_bjd_cd(bjd_nm=bjd_nm)
584
+ try:
585
+ if res.get("error") is not True:
586
+ return self.generate_pnu(
587
+ bjd_cd=res.get("bjd_cd"),
588
+ jibun=jibun
589
+ )
590
+ else:
591
+ error = res.get("error")
592
+ pnu = None
593
+ bjd_cd = None
594
+ mountain_cd = None
595
+ bunji_cd = None
596
+ bjd_datas = None
597
+ bun = None
598
+ ji = None
599
+ msg = f"올바르지 않은 법정동명({res.get('msg')})"
600
+
601
+ except Exception as e:
602
+ error = True
603
+ pnu = None
604
+ bjd_cd = None
605
+ mountain_cd = None
606
+ bunji_cd = None
607
+ bjd_datas = None
608
+ bun = None
609
+ ji = None
610
+ msg = str(e)
611
+
612
+ return {
613
+ "error": error,
614
+ "pnu": pnu,
615
+ "bjd_cd": bjd_cd,
616
+ "mountain_cd": mountain_cd,
617
+ "bunji_cd": bunji_cd,
618
+ "bjd_datas": bjd_datas,
619
+ "bun": bun,
620
+ "ji": ji,
621
+ "msg": msg,
622
+ "base_dt": self.base_dt_print
623
+ }
@@ -0,0 +1,42 @@
1
+ import logging
2
+
3
+
4
+ class Log:
5
+
6
+ def __init__(
7
+ self,
8
+ name: str
9
+ ):
10
+ self.log = logging.getLogger(name)
11
+ self.log.propagate = True
12
+ self.formatter = logging.Formatter("%(asctime)s | [%(levelname)s] | %(message)s",
13
+ "%Y-%m-%d %H:%M:%S")
14
+ self.levels = {
15
+ "DEBUG" : logging.DEBUG,
16
+ "INFO" : logging.INFO,
17
+ "WARNING" : logging.WARNING,
18
+ "ERROR" : logging.ERROR,
19
+ "CRITICAL" : logging.CRITICAL
20
+ }
21
+
22
+ def stream_handler(
23
+ self,
24
+ level: str
25
+ ):
26
+ if len(self.log.handlers) > 0:
27
+ return self.log # Logger already exists
28
+ else:
29
+ """
30
+ level :
31
+ > "DEBUG" : logging.DEBUG ,
32
+ > "INFO" : logging.INFO ,
33
+ > "WARNING" : logging.WARNING ,
34
+ > "ERROR" : logging.ERROR ,
35
+ > "CRITICAL" : logging.CRITICAL ,
36
+ """
37
+ self.log.setLevel(self.levels[level])
38
+ streamHandler = logging.StreamHandler()
39
+ streamHandler.setFormatter(self.formatter)
40
+ self.log.addHandler(streamHandler)
41
+ return self.log
42
+