db2_hj3415 0.1.6__py2.py3-none-any.whl → 0.1.8__py2.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.
@@ -1,8 +1,6 @@
1
- # MongoDB/Redis 연결
1
+ # MongoDB 연결
2
2
  import os
3
3
  from motor.motor_asyncio import AsyncIOMotorClient
4
- import redis.asyncio as redis
5
- from redis.asyncio.client import Redis
6
4
 
7
5
  # 싱글톤 몽고 클라이언트 정의
8
6
  MONGO_URI = os.getenv("MONGO_ADDR", "mongodb://localhost:27017")
@@ -19,11 +17,20 @@ def close_mongo_client():
19
17
  client.close()
20
18
 
21
19
 
22
- async def get_redis() -> Redis:
23
- client = redis.Redis(host="localhost", port=6379, decode_responses=True)
24
- try:
25
- await client.ping()
26
- return client
27
- except Exception as e:
28
- print("Redis connection failed:", e)
29
- return None
20
+ from pymongo import MongoClient
21
+
22
+ client_sync: MongoClient = None # 동기 클라이언트 타입으로 변경
23
+
24
+ def get_mongo_client_sync() -> MongoClient:
25
+ """
26
+ MongoDB 동기 클라이언트를 반환합니다.
27
+ 전역 client가 None일 경우 새 클라이언트를 생성합니다.
28
+ """
29
+ global client_sync
30
+ if client_sync is None:
31
+ client_sync = MongoClient(MONGO_URI)
32
+ return client_sync
33
+
34
+ def close_mongo_client_sync():
35
+ if client_sync:
36
+ client_sync.close()
@@ -11,14 +11,15 @@ from bson import json_util
11
11
  from pydantic import BaseModel
12
12
 
13
13
  def pretty_print(obj):
14
- if isinstance(obj, BaseModel):
15
- # Pydantic 모델이면 dict로 변환
16
- data = obj.model_dump(by_alias=True)
17
- elif isinstance(obj, list) and all(isinstance(o, BaseModel) for o in obj):
18
- # 리스트 안에 BaseModel만 있다면 변환
19
- data = [o.model_dump(by_alias=True) for o in obj]
20
- else:
21
- # 일반 dict나 기타 객체
22
- data = obj
14
+ def convert(o):
15
+ if isinstance(o, BaseModel):
16
+ return o.model_dump(by_alias=True)
17
+ if isinstance(o, dict):
18
+ return {k: convert(v) for k, v in o.items()}
19
+ if isinstance(o, list):
20
+ return [convert(v) for v in o]
21
+ return o # 기본값 (예: str, int, float 등)
22
+
23
+ data = convert(obj)
23
24
 
24
25
  print(json.dumps(data, indent=2, ensure_ascii=False, default=json_util.default))
@@ -4,5 +4,6 @@ DATE_FORMAT = "%Y.%m.%d"
4
4
  from db2_hj3415.common import connection
5
5
  from db2_hj3415.common.utils import *
6
6
  from db2_hj3415.common.db_ops import *
7
+
7
8
  from db2_hj3415.nfs._ops import *
8
9
  from db2_hj3415.nfs.models import *
db2_hj3415/nfs/_c10346.py CHANGED
@@ -62,7 +62,7 @@ async def _compare_and_log_diff(code: str, new_doc: dict, latest_doc: dict | Non
62
62
  return True
63
63
 
64
64
 
65
- def _prepare_c10346_document(code: str, data: dict[str, pd.DataFrame]) -> dict:
65
+ def _prepare_c10346_document_for_save(code: str, data: dict[str, pd.DataFrame]) -> dict:
66
66
  now = datetime.now(timezone.utc)
67
67
  document = {"코드": code, "날짜": now}
68
68
 
@@ -80,7 +80,7 @@ async def save(col: str, code: str, data: dict[str, pd.DataFrame], client: Async
80
80
 
81
81
  await collection.create_index([("코드", ASCENDING), ("날짜", ASCENDING)], unique=True)
82
82
 
83
- document = _prepare_c10346_document(code, data)
83
+ document = _prepare_c10346_document_for_save(code, data)
84
84
  latest_doc = await collection.find_one({"코드": code}, sort=[("날짜", DESCENDING)])
85
85
 
86
86
  need_save = await _compare_and_log_diff(code, document, latest_doc, client)
@@ -106,7 +106,7 @@ async def save_many(col: str, many_data: dict[str, dict[str, pd.DataFrame]], cli
106
106
  results = []
107
107
 
108
108
  for code, data in many_data.items():
109
- document = _prepare_c10346_document(code, data)
109
+ document = _prepare_c10346_document_for_save(code, data)
110
110
  latest_doc = await collection.find_one({"코드": code}, sort=[("날짜", DESCENDING)])
111
111
  need_save = await _compare_and_log_diff(code, document, latest_doc, client)
112
112
 
@@ -127,7 +127,7 @@ async def save_many(col: str, many_data: dict[str, dict[str, pd.DataFrame]], cli
127
127
  return results
128
128
 
129
129
 
130
- async def get_latest(col: str, code: str, client: AsyncIOMotorClient) -> C103 | C104 | C106 | None:
130
+ async def fetch_latest_doc(col: str, code: str, client: AsyncIOMotorClient) -> dict | None:
131
131
  collection = get_collection(client, DB_NAME, col)
132
132
 
133
133
  # 최신 날짜 기준으로 정렬하여 1건만 조회
@@ -142,19 +142,89 @@ async def get_latest(col: str, code: str, client: AsyncIOMotorClient) -> C103 |
142
142
 
143
143
  latest_doc["_id"] = str(latest_doc["_id"])
144
144
 
145
- try:
146
- match col:
147
- case 'c103':
148
- return C103(**latest_doc)
149
- case 'c104':
150
- return C104(**latest_doc)
151
- case 'c106':
152
- return C106(**latest_doc)
153
-
154
- except Exception as e:
155
- print(f"[{code}] C103 파싱 실패: {e}")
145
+ return latest_doc
146
+
147
+
148
+ async def get_latest_as_model(col: str, code: str, client: AsyncIOMotorClient) -> C103 | C104 | C106 | None:
149
+ latest_doc = await fetch_latest_doc(col, code, client)
150
+
151
+ if not latest_doc:
156
152
  return None
157
153
 
154
+ match col:
155
+ case 'c103':
156
+ return C103(**latest_doc)
157
+ case 'c104':
158
+ return C104(**latest_doc)
159
+ case 'c106':
160
+ return C106(**latest_doc)
161
+ case _:
162
+ raise ValueError(f"지원하지 않는 컬렉션 이름: {col}")
163
+
164
+
165
+ async def get_latest_doc_as_df_dict(col: str, code: str, client: AsyncIOMotorClient) -> dict[str, pd.DataFrame] | None:
166
+ latest_doc = await fetch_latest_doc(col, code, client)
167
+
168
+ if not latest_doc:
169
+ return None
170
+
171
+ keys_map = {
172
+ 'c103': [
173
+ "손익계산서y", "재무상태표y", "현금흐름표y",
174
+ "손익계산서q", "재무상태표q", "현금흐름표q"
175
+ ],
176
+ 'c104': [
177
+ '수익성y', '성장성y', '안정성y', '활동성y', '가치분석y',
178
+ '수익성q', '성장성q', '안정성q', '활동성q', '가치분석q'
179
+ ],
180
+ 'c106': ['q', 'y']
181
+ }
182
+
183
+ def make_df_dict(keys: list[str]) -> dict[str, pd.DataFrame]:
184
+ """latest_doc에서 주어진 키들을 기반으로 DataFrame 생성"""
185
+ return {
186
+ key: pd.DataFrame(latest_doc.get(key) or []) for key in keys
187
+ }
188
+
189
+ def concat_by_suffix(dfs: dict[str, pd.DataFrame]) -> dict[str, pd.DataFrame]:
190
+ """'q', 'y' 접미사 기준으로 그룹 후 하나의 DataFrame으로 합침"""
191
+ grouped: dict[str, list[pd.DataFrame]] = {'q': [], 'y': []}
192
+
193
+ for key, df in dfs.items():
194
+ if key.endswith('q'):
195
+ grouped['q'].append(df.assign(분류=key))
196
+ elif key.endswith('y'):
197
+ grouped['y'].append(df.assign(분류=key))
198
+
199
+ def clean_df(df: pd.DataFrame) -> pd.DataFrame:
200
+ # 모든 값이 NaN인 열 제거
201
+ return df.dropna(axis=1, how='all')
202
+
203
+ return {
204
+ period: (
205
+ pd.concat(
206
+ [clean_df(f) for f in frames if not f.empty],
207
+ ignore_index=True
208
+ ) if any(not f.empty for f in frames) else pd.DataFrame()
209
+ )
210
+ for period, frames in grouped.items()
211
+ }
212
+
213
+ # 2. 실제 처리 로직
214
+ match col:
215
+ case 'c103':
216
+ return make_df_dict(keys_map['c103'])
217
+
218
+ case 'c104':
219
+ raw_result = make_df_dict(keys_map['c104'])
220
+ return concat_by_suffix(raw_result)
221
+
222
+ case 'c106':
223
+ return make_df_dict(keys_map['c106'])
224
+
225
+ case _:
226
+ raise ValueError(f"지원하지 않는 컬렉션 이름: {col}")
227
+
158
228
 
159
229
  async def has_doc_changed(col: str, code: str, client: AsyncIOMotorClient) -> bool:
160
230
  """
db2_hj3415/nfs/_ops.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from motor.motor_asyncio import AsyncIOMotorClient
2
- from db2_hj3415.nfs import DB_NAME
2
+ from db2_hj3415.nfs import DB_NAME, connection
3
3
 
4
4
 
5
5
  async def get_all_codes(client: AsyncIOMotorClient) -> list[str]:
@@ -26,6 +26,27 @@ async def get_all_codes(client: AsyncIOMotorClient) -> list[str]:
26
26
  return list(s)
27
27
 
28
28
 
29
+ def get_all_codes_sync() -> list[str]:
30
+ """
31
+ c103, c104, c106 컬렉션에 모두 존재하는 코드의 리스트를 반환함.
32
+ """
33
+ client = connection.get_mongo_client_sync()
34
+ try:
35
+ db = client[DB_NAME]
36
+ collections = ['c103', 'c104', 'c106']
37
+
38
+ # 첫 컬렉션 코드 셋팅
39
+ common_codes = set(db[collections[0]].distinct("코드"))
40
+
41
+ for col in collections[1:]:
42
+ codes = db[col].distinct("코드")
43
+ common_codes &= set(codes)
44
+
45
+ return sorted(common_codes) # 필요에 따라 정렬
46
+ finally:
47
+ connection.close_mongo_client_sync()
48
+
49
+
29
50
  async def delete_code_from_all_collections(code: str, client: AsyncIOMotorClient) -> dict[str, int]:
30
51
  db = client[DB_NAME]
31
52
 
db2_hj3415/nfs/c101.py CHANGED
@@ -104,10 +104,18 @@ async def get_latest(code: str, client: AsyncIOMotorClient) -> C101 | None:
104
104
  mylogger.debug(doc)
105
105
  return C101(**doc)
106
106
  else:
107
- print(f"데이터 없음: {code}")
107
+ mylogger.warning(f"데이터 없음: {code}")
108
108
  return None
109
109
 
110
110
 
111
+ async def get_name(code: str, client: AsyncIOMotorClient) -> str | None:
112
+ c101_data = await get_latest(code, client)
113
+ if c101_data is None:
114
+ return None
115
+ else:
116
+ return c101_data.종목명
117
+
118
+
111
119
  SortOrder = Literal["asc", "desc"]
112
120
 
113
121
  async def get_all_data(code: str, client: AsyncIOMotorClient, sort: SortOrder = 'asc') -> list[C101]:
db2_hj3415/nfs/c103.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from motor.motor_asyncio import AsyncIOMotorClient
2
4
  import pandas as pd
3
5
 
@@ -17,8 +19,18 @@ async def save_many(many_data: dict[str, dict[str, pd.DataFrame]], client: Async
17
19
  return await _c10346.save_many(COL_NAME, many_data, client)
18
20
 
19
21
 
20
- async def get_latest(code: str, client: AsyncIOMotorClient) -> C103 | None:
21
- return await _c10346.get_latest(COL_NAME, code, client)
22
+ ReturnType = C103 | dict[str, pd.DataFrame] | None
23
+
24
+
25
+ async def get_latest(code: str, client: AsyncIOMotorClient, as_type: Literal["model", "dataframe"] = "model") -> ReturnType:
26
+ if as_type == "model":
27
+ return await _c10346.get_latest_as_model(COL_NAME, code, client)
28
+
29
+ elif as_type == "dataframe":
30
+ return await _c10346.get_latest_doc_as_df_dict(COL_NAME, code, client)
31
+
32
+ else:
33
+ raise ValueError(f"지원하지 않는 반환 타입: '{as_type}' (허용값: 'model', 'dataframe')")
22
34
 
23
35
 
24
36
  async def has_doc_changed(code: str, client: AsyncIOMotorClient) -> bool:
db2_hj3415/nfs/c104.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from motor.motor_asyncio import AsyncIOMotorClient
2
4
  import pandas as pd
3
5
 
@@ -17,8 +19,18 @@ async def save_many(many_data: dict[str, dict[str, pd.DataFrame]], client: Async
17
19
  return await _c10346.save_many(COL_NAME, many_data, client)
18
20
 
19
21
 
20
- async def get_latest(code: str, client: AsyncIOMotorClient) -> C104 | None:
21
- return await _c10346.get_latest(COL_NAME, code, client)
22
+ ReturnType = C104 | dict[str, pd.DataFrame] | None
23
+
24
+
25
+ async def get_latest(code: str, client: AsyncIOMotorClient, as_type: Literal["model", "dataframe"] = "model") -> ReturnType:
26
+ if as_type == "model":
27
+ return await _c10346.get_latest_as_model(COL_NAME, code, client)
28
+
29
+ elif as_type == "dataframe":
30
+ return await _c10346.get_latest_doc_as_df_dict(COL_NAME, code, client)
31
+
32
+ else:
33
+ raise ValueError(f"지원하지 않는 반환 타입: '{as_type}' (허용값: 'model', 'dataframe')")
22
34
 
23
35
 
24
36
  async def has_doc_changed(code: str, client: AsyncIOMotorClient) -> bool:
db2_hj3415/nfs/c106.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from motor.motor_asyncio import AsyncIOMotorClient
2
4
  import pandas as pd
3
5
 
@@ -17,8 +19,18 @@ async def save_many(many_data: dict[str, dict[str, pd.DataFrame]], client: Async
17
19
  return await _c10346.save_many(COL_NAME, many_data, client)
18
20
 
19
21
 
20
- async def get_latest(code: str, client: AsyncIOMotorClient) -> C106 | None:
21
- return await _c10346.get_latest(COL_NAME, code, client)
22
+ ReturnType = C106 | dict[str, pd.DataFrame] | None
23
+
24
+
25
+ async def get_latest(code: str, client: AsyncIOMotorClient, as_type: Literal["model", "dataframe"] = "model") -> ReturnType:
26
+ if as_type == "model":
27
+ return await _c10346.get_latest_as_model(COL_NAME, code, client)
28
+
29
+ elif as_type == "dataframe":
30
+ return await _c10346.get_latest_doc_as_df_dict(COL_NAME, code, client)
31
+
32
+ else:
33
+ raise ValueError(f"지원하지 않는 반환 타입: '{as_type}' (허용값: 'model', 'dataframe')")
22
34
 
23
35
 
24
36
  async def has_doc_changed(code: str, client: AsyncIOMotorClient) -> bool:
db2_hj3415/nfs/dart.py CHANGED
@@ -0,0 +1,27 @@
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+ from pymongo import InsertOne
3
+ from db2_hj3415.nfs import Dart, DB_NAME, get_collection
4
+
5
+ COL_NAME = "dart"
6
+
7
+ async def save_many(many_data: list[Dart], client: AsyncIOMotorClient) -> dict:
8
+ if not many_data:
9
+ return {"inserted_count": 0, "skipped": 0}
10
+
11
+ collection = get_collection(client, DB_NAME, COL_NAME)
12
+
13
+ # unique index 보장용 (이미 설정되었는지 확인)
14
+ await collection.create_index("rcept_no", unique=True)
15
+
16
+ ops = []
17
+ skipped = 0
18
+
19
+ for item in many_data:
20
+ doc = item.model_dump(mode="json", exclude={"id"}) # _id는 제외
21
+ ops.append(InsertOne(doc))
22
+
23
+ try:
24
+ result = await collection.bulk_write(ops, ordered=False)
25
+ return {"inserted_count": result.inserted_count, "skipped": skipped}
26
+ except Exception as e:
27
+ return {"error": str(e)}
db2_hj3415/nfs/models.py CHANGED
@@ -3,8 +3,9 @@ from datetime import datetime
3
3
 
4
4
  class C101(BaseModel):
5
5
  id: str | None = Field(default=None, alias="_id")
6
- 날짜: datetime
7
6
  코드: str
7
+ 날짜: datetime
8
+ 종목명: str | None
8
9
  bps: int | None
9
10
  eps: int | None
10
11
  pbr: float | None
@@ -26,7 +27,7 @@ class C101(BaseModel):
26
27
  외국인지분율: float | None
27
28
  유동비율: float | None
28
29
  전일대비: int | None
29
- 종목명: str | None
30
+
30
31
  주가: int | None
31
32
  최고52: int | None
32
33
  최저52: int | None
@@ -0,0 +1,9 @@
1
+ DB_NAME = "valuation"
2
+ DATE_FORMAT = "%Y.%m.%d"
3
+
4
+ from db2_hj3415.common import connection
5
+ from db2_hj3415.common.utils import *
6
+ from db2_hj3415.common.db_ops import *
7
+ from db2_hj3415.nfs._ops import get_all_codes_sync, get_all_codes
8
+
9
+ from db2_hj3415.valuation.models import *
@@ -0,0 +1,65 @@
1
+ from pymongo import ASCENDING
2
+ from motor.motor_asyncio import AsyncIOMotorClient
3
+ from datetime import datetime, timezone, time
4
+
5
+ from db2_hj3415.valuation import DB_NAME, MilData, RedData
6
+ from db2_hj3415.common.db_ops import get_collection
7
+ from utils_hj3415 import setup_logger
8
+ from bson import ObjectId
9
+
10
+ mylogger = setup_logger(__name__, 'WARNING')
11
+
12
+ T = RedData | MilData
13
+
14
+ async def save(col: str, data: T, client: AsyncIOMotorClient) -> dict:
15
+ collection = get_collection(client, DB_NAME, col)
16
+ await collection.create_index([("날짜", ASCENDING), ("코드", ASCENDING)], unique=True)
17
+
18
+ data.날짜 = datetime.now(timezone.utc)
19
+
20
+ # 날짜 기준 중복 확인
21
+ today = data.날짜.date()
22
+
23
+ existing = await collection.find_one({
24
+ "코드": data.코드,
25
+ "날짜": {
26
+ "$gte": datetime.combine(today, time.min).replace(tzinfo=timezone.utc),
27
+ "$lt": datetime.combine(today, time.max).replace(tzinfo=timezone.utc)
28
+ }
29
+ })
30
+ mylogger.debug(f"이미 저장된 오늘 날짜 데이터가 있나?: {existing}")
31
+
32
+ if existing:
33
+ return {"status": "skipped", "reason": "already_saved_today"}
34
+
35
+ # datetime 그대로 유지하기 위해 mode='python' 사용
36
+ doc = data.model_dump(by_alias=True, mode='python', exclude_none=False)
37
+
38
+ # ObjectId가 존재하면 업데이트, 아니면 삽입
39
+ if '_id' in doc:
40
+ if doc['_id'] is None:
41
+ doc.pop('_id') # None이면 제거하여 MongoDB에서 자동 생성되게 함
42
+ else:
43
+ doc['_id'] = ObjectId(doc['_id']) if isinstance(doc['_id'], str) else doc['_id']
44
+ await collection.replace_one({'_id': doc['_id']}, doc, upsert=True)
45
+ return {"status": "updated", "_id": str(doc['_id'])}
46
+
47
+ result = await collection.insert_one(doc)
48
+ data.id = str(result.inserted_id)
49
+ return {"status": "inserted", "_id": data.id}
50
+
51
+
52
+ async def save_many(col: str, many_data: dict[str, T], client: AsyncIOMotorClient) -> dict:
53
+ results = {}
54
+
55
+ # 이 방식이 속도는 느리지만 제일 간단하고 안정적인 방식임.
56
+ for code, data in many_data.items():
57
+ try:
58
+ result = await save(col, data, client)
59
+ results[code] = result
60
+ except Exception as e:
61
+ # 에러 발생 시 로깅 또는 실패 처리
62
+ results[code] = {"status": "error", "error": str(e)}
63
+ mylogger.error(f"[{code}] 저장 중 오류 발생: {e}")
64
+
65
+ return results
@@ -0,0 +1,17 @@
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+
3
+ from db2_hj3415.valuation import BlueData, _ops
4
+ from utils_hj3415 import setup_logger
5
+
6
+
7
+ mylogger = setup_logger(__name__, 'WARNING')
8
+
9
+ COL_NAME = "blue"
10
+
11
+
12
+ async def save(blue_data: BlueData, client: AsyncIOMotorClient) -> dict:
13
+ return await _ops.save(COL_NAME, blue_data, client)
14
+
15
+
16
+ async def save_many(many_data: dict[str, BlueData], client: AsyncIOMotorClient) -> dict:
17
+ return await _ops.save_many(COL_NAME, many_data, client)
@@ -0,0 +1,17 @@
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+
3
+ from db2_hj3415.valuation import GrowthData, _ops
4
+ from utils_hj3415 import setup_logger
5
+
6
+
7
+ mylogger = setup_logger(__name__, 'WARNING')
8
+
9
+ COL_NAME = "growth"
10
+
11
+
12
+ async def save(growth_data: GrowthData, client: AsyncIOMotorClient) -> dict:
13
+ return await _ops.save(COL_NAME, growth_data, client)
14
+
15
+
16
+ async def save_many(many_data: dict[str, GrowthData], client: AsyncIOMotorClient) -> dict:
17
+ return await _ops.save_many(COL_NAME, many_data, client)
@@ -0,0 +1,17 @@
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+
3
+ from db2_hj3415.valuation import MilData, _ops
4
+ from utils_hj3415 import setup_logger
5
+
6
+
7
+ mylogger = setup_logger(__name__, 'WARNING')
8
+
9
+ COL_NAME = "mil"
10
+
11
+
12
+ async def save(mil_data: MilData, client: AsyncIOMotorClient) -> dict:
13
+ return await _ops.save(COL_NAME, mil_data, client)
14
+
15
+
16
+ async def save_many(many_data: dict[str, MilData], client: AsyncIOMotorClient) -> dict:
17
+ return await _ops.save_many(COL_NAME, many_data, client)
@@ -0,0 +1,193 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, Field, field_validator, field_serializer, ConfigDict, model_validator, SerializationInfo
4
+ from datetime import datetime
5
+ import math
6
+ from utils_hj3415 import tools
7
+
8
+
9
+ class RedData(BaseModel):
10
+ id: str | None = Field(default=None, alias="_id")
11
+ 코드: str
12
+ 날짜: datetime | None = Field(default=None)
13
+ 종목명: str
14
+
15
+ 사업가치: float | None
16
+ 지배주주당기순이익: float | None
17
+ expect_earn: float | None
18
+
19
+ 재산가치: float | None
20
+ 유동자산: float | None
21
+ 유동부채: float | None
22
+ 투자자산: float | None
23
+ 투자부동산: float | None
24
+
25
+ 부채평가: float | None
26
+ 발행주식수: int | None
27
+
28
+ 자료제출일: list[str] | None = Field(default=None)
29
+ 주가: float | None
30
+ red_price: float | None
31
+ score: int | None
32
+
33
+ @model_validator(mode='before')
34
+ @classmethod
35
+ def replace_nan_with_none(cls, values: dict) -> dict:
36
+ return {
37
+ k: (None if isinstance(v, float) and math.isnan(v) else v)
38
+ for k, v in values.items()
39
+ }
40
+
41
+ @field_serializer("날짜")
42
+ def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
43
+ # JSON 응답용일 때만 문자열로 직렬화
44
+ if info.mode == 'json':
45
+ return value.isoformat()
46
+ return value
47
+
48
+ @field_validator("코드")
49
+ @classmethod
50
+ def validate_코드(cls, v):
51
+ if not tools.is_6digit(v):
52
+ raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
53
+ return v
54
+
55
+ model_config = ConfigDict(
56
+ populate_by_name=True,
57
+ str_strip_whitespace=True,
58
+ )
59
+
60
+
61
+ class Evaluation(BaseModel):
62
+ 최근값: float | None = Field(default=None)
63
+ 시계열: dict[str, float] = Field(default_factory=dict)
64
+ 평가결과: dict[str, Any] = Field(default_factory=dict)
65
+ model_config = ConfigDict(
66
+ extra="allow"
67
+ )
68
+
69
+
70
+ class MilData(BaseModel):
71
+ id: str | None = Field(default=None, alias="_id")
72
+ 코드: str
73
+ 날짜: datetime | None = Field(default=None)
74
+ 종목명: str
75
+
76
+ 주주수익률: float | None
77
+ 이익지표: float | None
78
+
79
+ # 투자수익률
80
+ ROIC: Evaluation
81
+ ROE: Evaluation
82
+ ROA: Evaluation
83
+
84
+ # 가치지표
85
+ FCF: Evaluation
86
+ PFCF: Evaluation
87
+ PCR: Evaluation
88
+
89
+ @model_validator(mode='before')
90
+ @classmethod
91
+ def replace_nan_with_none(cls, values: dict) -> dict:
92
+ return {
93
+ k: (None if isinstance(v, float) and math.isnan(v) else v)
94
+ for k, v in values.items()
95
+ }
96
+
97
+ @field_serializer("날짜")
98
+ def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
99
+ # JSON 응답용일 때만 문자열로 직렬화
100
+ if info.mode == 'json':
101
+ return value.isoformat()
102
+ return value
103
+
104
+ @field_validator("코드")
105
+ @classmethod
106
+ def validate_코드(cls, v):
107
+ if not tools.is_6digit(v):
108
+ raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
109
+ return v
110
+
111
+ model_config = ConfigDict(
112
+ populate_by_name=True,
113
+ str_strip_whitespace=True,
114
+ )
115
+
116
+
117
+ class BlueData(BaseModel):
118
+ id: str | None = Field(default=None, alias="_id")
119
+ 코드: str
120
+ 날짜: datetime | None = Field(default=None)
121
+ 종목명: str
122
+
123
+ 유동비율: float
124
+
125
+ 재고자산회전율: Evaluation
126
+ 이자보상배율: Evaluation
127
+ 순운전자본회전율: Evaluation | None
128
+ 순부채비율: Evaluation
129
+
130
+ 자료제출일: list[str] | None = Field(default=None)
131
+
132
+ @model_validator(mode='before')
133
+ @classmethod
134
+ def replace_nan_with_none(cls, values: dict) -> dict:
135
+ return {
136
+ k: (None if isinstance(v, float) and math.isnan(v) else v)
137
+ for k, v in values.items()
138
+ }
139
+
140
+ @field_serializer("날짜")
141
+ def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
142
+ # JSON 응답용일 때만 문자열로 직렬화
143
+ if info.mode == 'json':
144
+ return value.isoformat()
145
+ return value
146
+
147
+ @field_validator("코드")
148
+ @classmethod
149
+ def validate_코드(cls, v):
150
+ if not tools.is_6digit(v):
151
+ raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
152
+ return v
153
+
154
+ model_config = ConfigDict(
155
+ populate_by_name=True,
156
+ str_strip_whitespace=True,
157
+ )
158
+
159
+
160
+ class GrowthData(BaseModel):
161
+ id: str | None = Field(default=None, alias="_id")
162
+ 코드: str
163
+ 날짜: datetime | None = Field(default=None)
164
+ 종목명: str
165
+
166
+ 매출액증가율: Evaluation
167
+
168
+ @model_validator(mode='before')
169
+ @classmethod
170
+ def replace_nan_with_none(cls, values: dict) -> dict:
171
+ return {
172
+ k: (None if isinstance(v, float) and math.isnan(v) else v)
173
+ for k, v in values.items()
174
+ }
175
+
176
+ @field_serializer("날짜")
177
+ def serialize_날짜(self, value: datetime, info: SerializationInfo) -> str | datetime:
178
+ # JSON 응답용일 때만 문자열로 직렬화
179
+ if info.mode == 'json':
180
+ return value.isoformat()
181
+ return value
182
+
183
+ @field_validator("코드")
184
+ @classmethod
185
+ def validate_코드(cls, v):
186
+ if not tools.is_6digit(v):
187
+ raise ValueError(f"code는 6자리 숫자형 문자열이어야 합니다. (입력값: {v})")
188
+ return v
189
+
190
+ model_config = ConfigDict(
191
+ populate_by_name=True,
192
+ str_strip_whitespace=True,
193
+ )
@@ -0,0 +1,17 @@
1
+ from motor.motor_asyncio import AsyncIOMotorClient
2
+
3
+ from db2_hj3415.valuation import RedData, _ops
4
+ from utils_hj3415 import setup_logger
5
+
6
+
7
+ mylogger = setup_logger(__name__, 'WARNING')
8
+
9
+ COL_NAME = "red"
10
+
11
+
12
+ async def save(red_data: RedData, client: AsyncIOMotorClient) -> dict:
13
+ return await _ops.save(COL_NAME, red_data, client)
14
+
15
+
16
+ async def save_many(many_data: dict[str, RedData], client: AsyncIOMotorClient) -> dict:
17
+ return await _ops.save_many(COL_NAME, many_data, client)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: db2_hj3415
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Gathering the stock data by playwright
5
5
  Author-email: Hyungjin Kim <hj3415@gmail.com>
6
6
  Description-Content-Type: text/markdown
@@ -10,7 +10,6 @@ Requires-Dist: pandas
10
10
  Requires-Dist: pandas-stubs
11
11
  Requires-Dist: deepdiff
12
12
  Requires-Dist: utils_hj3415>=3.2.3
13
- Requires-Dist: redis
14
13
  Requires-Dist: pydantic
15
14
  Project-URL: Home, https://www.hyungjin.kr
16
15
 
@@ -0,0 +1,41 @@
1
+ db2_hj3415/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ db2_hj3415/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ db2_hj3415/cli/db.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ db2_hj3415/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ db2_hj3415/common/connection.py,sha256=Y3d4cn_pLkEpWo9B5E8l7FDot45dfBUdtsiwOxwO_o8,942
6
+ db2_hj3415/common/db_ops.py,sha256=CajXob5FofIzsuyFvtlOEo1UkdYMfsLUwKDmbb29Kas,212
7
+ db2_hj3415/common/utils.py,sha256=lpirdYGOK7s7TDYp_e_dErN-b75bFuAQZxdPilJpChA,766
8
+ db2_hj3415/mi/__init__.py,sha256=4XIwdkLfdUCiq2zLqCnR5ypuI45nzX02qez80xWQ70A,159
9
+ db2_hj3415/mi/_ops.py,sha256=Xm2ZpnKxAymIieut8JyK3epHED8uMlhO77ScACfoB7g,3988
10
+ db2_hj3415/mi/aud.py,sha256=w4w4EC0hn6kKHjNkeIUL5ZsvHH4IzRGBpTKRptHapVY,765
11
+ db2_hj3415/mi/chf.py,sha256=Sx8HCc0O0sfjiYmlfwozts0hm-bszkU0CgKe3-2fXbc,768
12
+ db2_hj3415/mi/gbond3y.py,sha256=XiHLm0hSYCkKaYxzL8e0OLNyl7AECHEbudhols-EaEU,772
13
+ db2_hj3415/mi/gold.py,sha256=_XCUdHZPy3jQm_U8A2iBrzBAMtW5B-dL467w0BLL2E0,769
14
+ db2_hj3415/mi/kosdaq.py,sha256=gWsn7B0U5kCE7EHTpUGKw9snS6QlqqwbxTjR9J_PgC8,816
15
+ db2_hj3415/mi/kospi.py,sha256=iL1DDzPzJdpPqwdU3sejkUFcpQ6bgqnljzKgGgDghik,815
16
+ db2_hj3415/mi/silver.py,sha256=Vh91LUY2Pwi4JsObnzrLVsZqSwagDTgcJQP4ugBWMTw,771
17
+ db2_hj3415/mi/sp500.py,sha256=2OFCo6SxXgLijOGLQhV9Ygh0OR10IviGXI0EjKcwfOI,800
18
+ db2_hj3415/mi/usdidx.py,sha256=cT6w-Y-Ysv0KI9yeFqDQYCdT0KPElwn8IZ0JF5oZ_MA,550
19
+ db2_hj3415/mi/usdkrw.py,sha256=QjcHqFSpSgyzI5lzRfYycNUzolzM-ikOvPDFy4FPadM,876
20
+ db2_hj3415/mi/wti.py,sha256=wMoS5yHGOlUS2b2P7Eok5U_59eehYXQGfqWa56J_dkk,768
21
+ db2_hj3415/nfs/__init__.py,sha256=JA3ZZHIKFTzDkRp2DC_WCBocotjU5ZlNTFT3JrastXI,230
22
+ db2_hj3415/nfs/_c10346.py,sha256=9cb6yZp5ZfPY5JZO7_4BuHiV_md4yDrM3XFrUf7oH5g,9392
23
+ db2_hj3415/nfs/_ops.py,sha256=zq4bPA69jYnUOezD9AfY95ZHuAWQZZZWLARkKFioOu0,1833
24
+ db2_hj3415/nfs/c101.py,sha256=5gndh6OAQPHc6cvBBnBqdfHuMt34oEoFaXFOdCRv62I,5077
25
+ db2_hj3415/nfs/c103.py,sha256=HfbknyRcC7-mKF5vnZzlHnESl9fBRwoIEZ_WCy6-tsM,1864
26
+ db2_hj3415/nfs/c104.py,sha256=yYV2dxble1pXx4f_X7q6bKY54WDHgJ3btTbvX0C3Mik,1864
27
+ db2_hj3415/nfs/c106.py,sha256=lzbMROOQ-w1AU8oeSYsYFhYuk0EuOux97CKuJrWhx1A,1864
28
+ db2_hj3415/nfs/c108.py,sha256=F_kP1HMhpe9E6VKm_m-aDl6PZ-FqVs5-O2_-rU8f34Y,3761
29
+ db2_hj3415/nfs/dart.py,sha256=yaqs7Qt8AYzbKWdPSNsaryOBOpVmNudTooBEaj2N-aY,876
30
+ db2_hj3415/nfs/models.py,sha256=Yx6KYELS9D0DoA6MNZ-b1snzaR8c3ywgF3Fr34BUHfs,4935
31
+ db2_hj3415/valuation/__init__.py,sha256=SxzWnyeo412A6q7yqYHtQz0Qv-haoAN6qr5Q4h2NRqw,274
32
+ db2_hj3415/valuation/_ops.py,sha256=NuTtlLo30lGaiTPHKJP81yP8nfifFvEneCUUjChhLlQ,2480
33
+ db2_hj3415/valuation/blue.py,sha256=Y2ItiKcQ-dVnbVfVaNSx3F4JgEB82Mk6DBJ6dftDZIM,486
34
+ db2_hj3415/valuation/growth.py,sha256=bpixczYpGx6cfb2ChsZXM8YkNDrHMaNWdkuMyPZuldI,498
35
+ db2_hj3415/valuation/mil.py,sha256=kOc9MYrzHYTL7oBSOI95gTD2Edu3H2yOd3R-0qdaH8c,480
36
+ db2_hj3415/valuation/models.py,sha256=fBmWhtdUE56E7AaFs-04aw4WC8ptm-Thl_IX7Yex0js,5592
37
+ db2_hj3415/valuation/red.py,sha256=YV3uavn0A6-T5fk5YiD5MdpC2XhjZMN1mP-e2n_VJrs,481
38
+ db2_hj3415-0.1.8.dist-info/entry_points.txt,sha256=JAGFsxKtvbNgaKCSKo-QRYwCyToabWmdRW0MZWtg9kg,45
39
+ db2_hj3415-0.1.8.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
40
+ db2_hj3415-0.1.8.dist-info/METADATA,sha256=XRpS_YwHq0JELu1txD4bIRFeHmXce-BFILOqOdJ2P40,688
41
+ db2_hj3415-0.1.8.dist-info/RECORD,,
@@ -1,34 +0,0 @@
1
- db2_hj3415/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- db2_hj3415/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- db2_hj3415/cli/db.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- db2_hj3415/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- db2_hj3415/common/connection.py,sha256=QpjDtDHy6By0MCj81z5upJkWeuIgqeIsbXsihHcpKX4,774
6
- db2_hj3415/common/db_ops.py,sha256=CajXob5FofIzsuyFvtlOEo1UkdYMfsLUwKDmbb29Kas,212
7
- db2_hj3415/common/utils.py,sha256=TfbsAR1FWgbuxZUybXRdrFg6QSeJ8DInn6b8aw_NagE,805
8
- db2_hj3415/mi/__init__.py,sha256=4XIwdkLfdUCiq2zLqCnR5ypuI45nzX02qez80xWQ70A,159
9
- db2_hj3415/mi/_ops.py,sha256=Xm2ZpnKxAymIieut8JyK3epHED8uMlhO77ScACfoB7g,3988
10
- db2_hj3415/mi/aud.py,sha256=w4w4EC0hn6kKHjNkeIUL5ZsvHH4IzRGBpTKRptHapVY,765
11
- db2_hj3415/mi/chf.py,sha256=Sx8HCc0O0sfjiYmlfwozts0hm-bszkU0CgKe3-2fXbc,768
12
- db2_hj3415/mi/gbond3y.py,sha256=XiHLm0hSYCkKaYxzL8e0OLNyl7AECHEbudhols-EaEU,772
13
- db2_hj3415/mi/gold.py,sha256=_XCUdHZPy3jQm_U8A2iBrzBAMtW5B-dL467w0BLL2E0,769
14
- db2_hj3415/mi/kosdaq.py,sha256=gWsn7B0U5kCE7EHTpUGKw9snS6QlqqwbxTjR9J_PgC8,816
15
- db2_hj3415/mi/kospi.py,sha256=iL1DDzPzJdpPqwdU3sejkUFcpQ6bgqnljzKgGgDghik,815
16
- db2_hj3415/mi/silver.py,sha256=Vh91LUY2Pwi4JsObnzrLVsZqSwagDTgcJQP4ugBWMTw,771
17
- db2_hj3415/mi/sp500.py,sha256=2OFCo6SxXgLijOGLQhV9Ygh0OR10IviGXI0EjKcwfOI,800
18
- db2_hj3415/mi/usdidx.py,sha256=cT6w-Y-Ysv0KI9yeFqDQYCdT0KPElwn8IZ0JF5oZ_MA,550
19
- db2_hj3415/mi/usdkrw.py,sha256=QjcHqFSpSgyzI5lzRfYycNUzolzM-ikOvPDFy4FPadM,876
20
- db2_hj3415/mi/wti.py,sha256=wMoS5yHGOlUS2b2P7Eok5U_59eehYXQGfqWa56J_dkk,768
21
- db2_hj3415/nfs/__init__.py,sha256=w5QUgr_qWQNHRVcLrT8G8Rx6pNry2PnHfyezBDI5D2A,229
22
- db2_hj3415/nfs/_c10346.py,sha256=yKBOA4QkY2eAUMzOiYWz4BB1c4cW6X4s4D2Nj9rAgOA,6939
23
- db2_hj3415/nfs/_ops.py,sha256=lqrBBhEFcPupB2dQBXZU-Gftvd9dYTlmgJGTb5LdX-o,1193
24
- db2_hj3415/nfs/c101.py,sha256=ORCNXr3pq43zn4naSnyu1erDxFmDc2HLpXO19gv0KNo,4853
25
- db2_hj3415/nfs/c103.py,sha256=NY2e_BTfSHkELIEUJ8XhhYdNETQaRGOrF4qNaewB2_E,1460
26
- db2_hj3415/nfs/c104.py,sha256=aVLchUoxomlthS7FZ0adskBXmxIWQISGfc2EgWkWZDI,1460
27
- db2_hj3415/nfs/c106.py,sha256=dAVSnEM5E-sJZExMTfqDoQlozTtH05JOtenzbIUdvtQ,1460
28
- db2_hj3415/nfs/c108.py,sha256=F_kP1HMhpe9E6VKm_m-aDl6PZ-FqVs5-O2_-rU8f34Y,3761
29
- db2_hj3415/nfs/dart.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- db2_hj3415/nfs/models.py,sha256=nzoFbkoC58JGL79kcTZyTBheSRAdhPTDZZO36aDi3HU,4934
31
- db2_hj3415-0.1.6.dist-info/entry_points.txt,sha256=JAGFsxKtvbNgaKCSKo-QRYwCyToabWmdRW0MZWtg9kg,45
32
- db2_hj3415-0.1.6.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
33
- db2_hj3415-0.1.6.dist-info/METADATA,sha256=XncrQbOZUZGqR8KHd_wweoeIpoyL8zdQmcVWKH2KOEI,709
34
- db2_hj3415-0.1.6.dist-info/RECORD,,