krx-hj3415 2.0.0__py3-none-any.whl → 2.2.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.
krx_hj3415/__init__.py ADDED
File without changes
@@ -1,10 +1,15 @@
1
- # krx/cli.py
1
+ # krx_hj3415/cli.py
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
5
5
  import typer
6
6
 
7
- from .universe_service import refresh_and_diff, apply_removed_to_nfs
7
+ from db2_hj3415.mongo import mongo_from_env
8
+ from db2_hj3415.settings import get_settings
9
+ from db2_hj3415.universe.repo import ensure_indexes as ensure_indexes_universe
10
+ from db2_hj3415.nfs.repo import ensure_indexes as ensure_indexes_nfs
11
+
12
+ from krx_hj3415.usecases.sync_universe import run_sync, apply_removed
8
13
 
9
14
  app = typer.Typer(no_args_is_help=True)
10
15
 
@@ -27,15 +32,10 @@ async def _maybe_await_close(mongo: object) -> None:
27
32
  await out
28
33
 
29
34
 
30
- async def _bootstrap_indexes(db) -> None:
31
- from db2.settings import get_settings
32
- from db2.nfs import ensure_indexes
33
- from db2.universe import ensure_indexes_universe # 네 db2에 구현돼있다는 가정
34
-
35
+ async def _mongo_bootstrap(db) -> None:
35
36
  s = get_settings()
36
- snapshot_ttl_days = getattr(s, "MONGO_SNAPSHOT_TTL_DAYS", None)
37
- await ensure_indexes(db, snapshot_ttl_days=snapshot_ttl_days)
38
- await ensure_indexes_universe(db)
37
+ await ensure_indexes_universe(db, snapshot_ttl_days=s.SNAPSHOT_TTL_DAYS)
38
+ await ensure_indexes_nfs(db, snapshot_ttl_days=s.SNAPSHOT_TTL_DAYS)
39
39
 
40
40
 
41
41
  @app.command()
@@ -53,14 +53,12 @@ def sync(
53
53
  """
54
54
 
55
55
  async def _run():
56
- from db2.mongo import mongo_from_env
57
-
58
56
  mongo = mongo_from_env()
59
57
  db = mongo.get_db()
60
58
  try:
61
- await _bootstrap_indexes(db)
59
+ await _mongo_bootstrap(db)
62
60
 
63
- d = await refresh_and_diff(db, universe=universe, max_days=max_days, snapshot=snapshot)
61
+ d = await run_sync(db, universe=universe, max_days=max_days, snapshot=snapshot)
64
62
 
65
63
  typer.echo(f"\n=== UNIVERSE SYNC: {d.universe} ===")
66
64
  typer.echo(f"asof: {d.asof.isoformat()}")
@@ -82,7 +80,7 @@ def sync(
82
80
 
83
81
  if apply and d.removed:
84
82
  typer.echo("\n=== APPLY REMOVED TO NFS ===")
85
- r = await apply_removed_to_nfs(db, removed_codes=d.removed_codes)
83
+ r = await apply_removed(db, removed_codes=d.removed_codes)
86
84
  typer.echo(
87
85
  f"latest_deleted={r.get('latest_deleted', 0)}, "
88
86
  f"snapshots_deleted={r.get('snapshots_deleted', 0)}"
@@ -0,0 +1,33 @@
1
+ # krx_hj3415/domain/diff.py
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+ from typing import Iterable
6
+ from .types import UniverseDiff, CodeItem
7
+
8
+
9
+ def _to_code_map(items: Iterable[CodeItem]) -> dict[str, CodeItem]:
10
+ return {it.code: it for it in items}
11
+
12
+
13
+ def diff_universe(
14
+ *,
15
+ universe: str,
16
+ asof: datetime,
17
+ new_items: list[CodeItem],
18
+ old_items: list[CodeItem],
19
+ ) -> UniverseDiff:
20
+ new_map = _to_code_map(new_items)
21
+ old_map = _to_code_map(old_items)
22
+
23
+ added = [new_map[c] for c in sorted(new_map.keys() - old_map.keys())]
24
+ removed = [old_map[c] for c in sorted(old_map.keys() - new_map.keys())]
25
+ kept = len(new_map.keys() & old_map.keys())
26
+
27
+ return UniverseDiff(
28
+ universe=universe,
29
+ asof=asof,
30
+ added=added,
31
+ removed=removed,
32
+ kept_count=kept,
33
+ )
@@ -0,0 +1,30 @@
1
+ # krx_hj3415/domain/types.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CodeItem:
10
+ code: str
11
+ name: str
12
+ asof: datetime
13
+ market: str | None = None # 확장 대비(예: KRX/KOSPI/KOSDAQ/NYSE/NASDAQ 등)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class UniverseDiff:
18
+ universe: str
19
+ asof: datetime
20
+ added: list[CodeItem]
21
+ removed: list[CodeItem]
22
+ kept_count: int
23
+
24
+ @property
25
+ def added_codes(self) -> list[str]:
26
+ return [x.code for x in self.added]
27
+
28
+ @property
29
+ def removed_codes(self) -> list[str]:
30
+ return [x.code for x in self.removed]
@@ -1,35 +1,29 @@
1
- #krx/samsungfund.py
1
+ # krx_hj3415/provider/krx300_samsungfund_excel.py
2
2
  from __future__ import annotations
3
3
 
4
4
  import random
5
5
  import time
6
- from dataclasses import dataclass
7
- from datetime import datetime, timedelta, timezone
8
- from io import BytesIO
9
- from typing import Iterable
10
-
11
6
  import pandas as pd
12
7
  import requests
8
+ from datetime import datetime, timedelta, timezone
9
+ from io import BytesIO
13
10
 
14
- from .models import CodeItem
11
+ from domain_hj3415.common.time import utcnow
12
+ from krx_hj3415.domain.types import CodeItem
15
13
 
16
14
 
17
15
  FUND_ID = "2ETFA4"
18
16
  BASE_URL = "https://www.samsungfund.com/excel_pdf.do"
19
17
 
20
18
 
21
- def _utcnow() -> datetime:
22
- return datetime.now(timezone.utc)
23
-
24
-
25
19
  def _looks_like_excel(content: bytes) -> bool:
26
20
  # xls(ole) or xlsx(zip) 대충 체크
27
- return content.startswith(b"\xD0\xCF\x11\xE0") or content.startswith(b"PK\x03\x04")
21
+ return content.startswith(b"\xd0\xcf\x11\xe0") or content.startswith(b"PK\x03\x04")
28
22
 
29
23
 
30
24
  def find_valid_url(*, max_days: int = 15, timeout: int = 8) -> tuple[str, datetime]:
31
25
  for delta in range(1, max_days + 1):
32
- day = (_utcnow() - timedelta(days=delta)).astimezone(timezone.utc)
26
+ day = (utcnow() - timedelta(days=delta)).astimezone(timezone.utc)
33
27
  date_str = day.strftime("%Y%m%d")
34
28
  url = f"{BASE_URL}?fId={FUND_ID}&gijunYMD={date_str}"
35
29
 
@@ -84,4 +78,4 @@ def parse_krx300_items(excel_bytes: bytes, *, asof: datetime) -> list[CodeItem]:
84
78
  def fetch_krx300_items(*, max_days: int = 15) -> tuple[datetime, list[CodeItem]]:
85
79
  excel_bytes, asof_day, _url = download_excel_bytes(max_days=max_days)
86
80
  items = parse_krx300_items(excel_bytes, asof=asof_day)
87
- return asof_day, items
81
+ return asof_day, items
@@ -1,23 +1,24 @@
1
- # krx/universe_service.py
1
+ # krx_hj3415/usecases/sync_universe.py
2
2
  from __future__ import annotations
3
3
 
4
- from dataclasses import asdict
5
- from datetime import datetime, timezone
6
- from typing import Any, Iterable
7
-
4
+ from datetime import datetime
5
+ from typing import Any, Iterable, cast
8
6
  from pymongo.asynchronous.database import AsyncDatabase
9
7
 
10
- from .models import CodeItem, UniverseDiff, diff_universe
11
- from .samsungfund import fetch_krx300_items
12
-
8
+ from domain_hj3415.common.time import utcnow
13
9
 
14
- def _utcnow() -> datetime:
15
- return datetime.now(timezone.utc)
10
+ from db2_hj3415.nfs.repo import delete_codes_from_nfs
11
+ from db2_hj3415.universe.repo import (
12
+ upsert_latest as upsert_universe_latest,
13
+ insert_snapshot as insert_universe_snapshot,
14
+ )
15
+ from db2_hj3415.universe.repo import get_latest as get_universe_latest
16
16
 
17
+ from contracts_hj3415.universe.dto import UniverseItemDTO, Market
17
18
 
18
- def _items_to_payload(items: list[CodeItem]) -> list[dict[str, Any]]:
19
- # db2에 저장할 때는 dict로 바꿔서 넣는 게 가장 단순함
20
- return [asdict(it) for it in items]
19
+ from krx_hj3415.domain.types import CodeItem, UniverseDiff
20
+ from krx_hj3415.domain.diff import diff_universe
21
+ from krx_hj3415.provider.krx300_samsungfund_excel import fetch_krx300_items
21
22
 
22
23
 
23
24
  def _payload_to_items(payload: Any) -> list[CodeItem]:
@@ -32,7 +33,11 @@ def _payload_to_items(payload: Any) -> list[CodeItem]:
32
33
  return []
33
34
 
34
35
  data = payload
35
- if isinstance(data, dict) and "payload" in data and isinstance(data["payload"], dict):
36
+ if (
37
+ isinstance(data, dict)
38
+ and "payload" in data
39
+ and isinstance(data["payload"], dict)
40
+ ):
36
41
  data = data["payload"]
37
42
 
38
43
  if isinstance(data, dict) and "items" in data:
@@ -56,7 +61,7 @@ def _payload_to_items(payload: Any) -> list[CodeItem]:
56
61
  asof = asof_raw
57
62
  else:
58
63
  # fallback: 현재 시각 (정확도가 중요하면 ISO parse 추가)
59
- asof = _utcnow()
64
+ asof = utcnow()
60
65
  out.append(CodeItem(code=code, name=name, asof=asof, market=market))
61
66
  return out
62
67
 
@@ -66,7 +71,31 @@ async def refresh_krx300(*, max_days: int = 15) -> tuple[datetime, list[CodeItem
66
71
  return fetch_krx300_items(max_days=max_days)
67
72
 
68
73
 
69
- async def refresh_and_diff(
74
+ def to_universe_item_dtos(items: Iterable[Any], *, market: str = "KRX") -> list[UniverseItemDTO]:
75
+ out: list[UniverseItemDTO] = []
76
+ for it in items:
77
+ if it is None:
78
+ continue
79
+
80
+ if isinstance(it, dict):
81
+ code = (it.get("code") or "").strip()
82
+ name = it.get("name")
83
+ else:
84
+ code = (getattr(it, "code", "") or "").strip()
85
+ name = getattr(it, "name", None)
86
+
87
+ if not code:
88
+ continue
89
+
90
+ dto: UniverseItemDTO = {"code": code, "market": cast(Market, market)}
91
+ if isinstance(name, str) and name.strip():
92
+ dto["name"] = name.strip()
93
+
94
+ out.append(dto)
95
+ return out
96
+
97
+
98
+ async def run_sync(
70
99
  db: AsyncDatabase,
71
100
  *,
72
101
  universe: str = "krx300",
@@ -86,23 +115,27 @@ async def refresh_and_diff(
86
115
  asof, new_items = await refresh_krx300(max_days=max_days)
87
116
 
88
117
  # --- 2) load old ---
89
- from db2.universe import get_universe_latest # ✅ db2에 구현돼있다고 가정
90
118
  old_doc = await get_universe_latest(db, universe=universe)
91
119
  old_items = _payload_to_items(old_doc)
92
120
 
93
121
  # --- 3) diff ---
94
- d = diff_universe(universe=universe, asof=asof, new_items=new_items, old_items=old_items)
122
+ d = diff_universe(
123
+ universe=universe, asof=asof, new_items=new_items, old_items=old_items
124
+ )
95
125
 
96
126
  # --- 4) save ---
97
- from db2.universe import upsert_universe_latest, insert_universe_snapshot # ✅ 가정
98
- await upsert_universe_latest(db, universe=universe, items=_items_to_payload(new_items), asof=asof)
127
+ await upsert_universe_latest(
128
+ db, universe=universe, items=to_universe_item_dtos(new_items), asof=asof
129
+ )
99
130
  if snapshot:
100
- await insert_universe_snapshot(db, universe=universe, items=_items_to_payload(new_items), asof=asof)
131
+ await insert_universe_snapshot(
132
+ db, universe=universe, items=to_universe_item_dtos(new_items), asof=asof
133
+ )
101
134
 
102
135
  return d
103
136
 
104
137
 
105
- async def apply_removed_to_nfs(
138
+ async def apply_removed(
106
139
  db: AsyncDatabase,
107
140
  *,
108
141
  removed_codes: Iterable[str],
@@ -114,12 +147,4 @@ async def apply_removed_to_nfs(
114
147
  if not codes:
115
148
  return {"latest_deleted": 0, "snapshots_deleted": 0}
116
149
 
117
- # 너가 db2.nfs에 추가한 “명확한 의도 API”로 맞추는 걸 추천
118
- # 예: delete_codes_from_all_endpoints(db, codes)
119
- try:
120
- from db2.nfs import delete_codes_from_all_endpoints # ✅ 네가 추가했다고 가정
121
- return await delete_codes_from_all_endpoints(db, codes=codes)
122
- except Exception:
123
- # fallback: 이미 있는 delete_codes_from_nfs(endpoint=None) 사용
124
- from db2.nfs import delete_codes_from_nfs
125
- return await delete_codes_from_nfs(db, codes=codes, endpoint=None) # type: ignore[arg-type]
150
+ return await delete_codes_from_nfs(db, codes=codes, endpoint=None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: krx-hj3415
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: KRX300 code scraper
5
5
  Keywords: example,demo
6
6
  Author-email: Hyungjin Kim <hj3415@gmail.com>
@@ -0,0 +1,11 @@
1
+ krx_hj3415/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ krx_hj3415/cli.py,sha256=F6ZpX8k1fNwtsRv59ixv3d1dPSnmWyxwXZbzu61ViWI,3174
3
+ krx_hj3415/domain/diff.py,sha256=m6nl73Anjx2si6vr9a-FXdH5iKVHh_4kXF9RHgplSdY,847
4
+ krx_hj3415/domain/types.py,sha256=-ERCYZXLX0LGjqEthcrBUpNWgBf2tJ5NgHE5Zxg2pls,663
5
+ krx_hj3415/provider/krx300_samsungfund_excel.py,sha256=WCQAeBsED5Cp9-4lZcptifwX1yY_-XRAP4iO8w5kb0A,2776
6
+ krx_hj3415/usecases/sync_universe.py,sha256=jlhMdKJsFbBD87kX0Y4eiuQdxVDWcpv07OJ45_bMFlg,4489
7
+ krx_hj3415-2.2.0.dist-info/entry_points.txt,sha256=qasj01Y1e0MIVlUXOraAZCAxDTn07CyMVV58hrYDamA,42
8
+ krx_hj3415-2.2.0.dist-info/licenses/LICENSE,sha256=QBiVGQuKAESeCfQE344Ik2ex6g2zfYdu9WqrRWydxIs,1068
9
+ krx_hj3415-2.2.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
10
+ krx_hj3415-2.2.0.dist-info/METADATA,sha256=ob5DNLcwjFtsTKN5dTpEVNS06YKpJAc-qZV54I4Wxw4,2309
11
+ krx_hj3415-2.2.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ krx=krx_hj3415.cli:app
3
+
krx/__init__.py DELETED
@@ -1,12 +0,0 @@
1
- # krx/__init__.py
2
- from .models import CodeItem, UniverseDiff
3
- from .samsungfund import fetch_krx300_items
4
- from .universe_service import refresh_and_diff, apply_removed_to_nfs
5
-
6
- __all__ = [
7
- "CodeItem",
8
- "UniverseDiff",
9
- "fetch_krx300_items",
10
- "refresh_and_diff",
11
- "apply_removed_to_nfs",
12
- ]
krx/models.py DELETED
@@ -1,52 +0,0 @@
1
- # krx/models.py
2
- from __future__ import annotations
3
-
4
- from dataclasses import dataclass
5
- from datetime import datetime
6
- from typing import Any, Iterable
7
-
8
-
9
- @dataclass(frozen=True)
10
- class CodeItem:
11
- code: str
12
- name: str
13
- asof: datetime
14
- market: str | None = None # 확장 대비(예: KRX/KOSPI/KOSDAQ/NYSE/NASDAQ 등)
15
-
16
-
17
- @dataclass(frozen=True)
18
- class UniverseDiff:
19
- universe: str
20
- asof: datetime
21
- added: list[CodeItem]
22
- removed: list[CodeItem]
23
- kept_count: int
24
-
25
- @property
26
- def added_codes(self) -> list[str]:
27
- return [x.code for x in self.added]
28
-
29
- @property
30
- def removed_codes(self) -> list[str]:
31
- return [x.code for x in self.removed]
32
-
33
-
34
- def _to_code_map(items: Iterable[CodeItem]) -> dict[str, CodeItem]:
35
- return {it.code: it for it in items}
36
-
37
-
38
- def diff_universe(*, universe: str, asof: datetime, new_items: list[CodeItem], old_items: list[CodeItem]) -> UniverseDiff:
39
- new_map = _to_code_map(new_items)
40
- old_map = _to_code_map(old_items)
41
-
42
- added = [new_map[c] for c in sorted(new_map.keys() - old_map.keys())]
43
- removed = [old_map[c] for c in sorted(old_map.keys() - new_map.keys())]
44
- kept = len(new_map.keys() & old_map.keys())
45
-
46
- return UniverseDiff(
47
- universe=universe,
48
- asof=asof,
49
- added=added,
50
- removed=removed,
51
- kept_count=kept,
52
- )
@@ -1,10 +0,0 @@
1
- krx/__init__.py,sha256=eqkijmpo1DwxMnxdBMYwgOj7yGynD6F8m8WjZpZ8RSk,302
2
- krx/cli.py,sha256=dE9n_AYZxT8VIndlitLsTJmp5T59lAGqLeV64P0WfJ4,3191
3
- krx/models.py,sha256=l35-C8jH32UOEBnHhFklE13P5ZDTg9Wft__WhSSqbxg,1342
4
- krx/samsungfund.py,sha256=m6add740cBQFgxZ5rW9xYXp_JBKKK03Y-bzb01GVq4w,2815
5
- krx/universe_service.py,sha256=xBhDSl1iGBgSQDZ8x7SZ8zzz2H4eS5j19HKOgxMJggo,4224
6
- krx_hj3415-2.0.0.dist-info/entry_points.txt,sha256=OR4WCHhNKwt2oBeySB2bSapcjl5SADj7kvEcxo44dPo,35
7
- krx_hj3415-2.0.0.dist-info/licenses/LICENSE,sha256=QBiVGQuKAESeCfQE344Ik2ex6g2zfYdu9WqrRWydxIs,1068
8
- krx_hj3415-2.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
9
- krx_hj3415-2.0.0.dist-info/METADATA,sha256=XvX6CAl-qmD4ROEoO4OZz3oxXv1QQm7sFbYkIr73Xiw,2309
10
- krx_hj3415-2.0.0.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- krx=krx.cli:app
3
-