krx-hj3415 2.2.3__tar.gz → 2.3.0__tar.gz

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.
Files changed (22) hide show
  1. {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/PKG-INFO +2 -2
  2. {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/pyproject.toml +2 -2
  3. krx_hj3415-2.3.0/src/krx_hj3415/app/adapters/nfs_store_db2.py +16 -0
  4. krx_hj3415-2.3.0/src/krx_hj3415/app/adapters/universe_store_db2.py +30 -0
  5. {krx_hj3415-2.2.3/src/krx_hj3415 → krx_hj3415-2.3.0/src/krx_hj3415/app}/domain/diff.py +3 -3
  6. {krx_hj3415-2.2.3/src/krx_hj3415 → krx_hj3415-2.3.0/src/krx_hj3415/app}/domain/types.py +9 -3
  7. krx_hj3415-2.3.0/src/krx_hj3415/app/ports/__init__.py +0 -0
  8. krx_hj3415-2.3.0/src/krx_hj3415/app/ports/nfs_cleaner.py +8 -0
  9. krx_hj3415-2.3.0/src/krx_hj3415/app/ports/universe_store.py +17 -0
  10. krx_hj3415-2.3.0/src/krx_hj3415/app/provider/__init__.py +0 -0
  11. {krx_hj3415-2.2.3/src/krx_hj3415 → krx_hj3415-2.3.0/src/krx_hj3415/app}/provider/krx300_samsungfund_excel.py +5 -5
  12. krx_hj3415-2.3.0/src/krx_hj3415/app/usecases/__init__.py +0 -0
  13. krx_hj3415-2.3.0/src/krx_hj3415/app/usecases/sync_universe.py +118 -0
  14. {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/src/krx_hj3415/cli.py +32 -10
  15. krx_hj3415-2.2.3/src/krx_hj3415/domain/universe.py +0 -8
  16. krx_hj3415-2.2.3/src/krx_hj3415/usecases/sync_universe.py +0 -136
  17. {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/LICENSE +0 -0
  18. {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/README.md +0 -0
  19. {krx_hj3415-2.2.3 → krx_hj3415-2.3.0}/src/krx_hj3415/__init__.py +0 -0
  20. {krx_hj3415-2.2.3/src/krx_hj3415/domain → krx_hj3415-2.3.0/src/krx_hj3415/app}/__init__.py +0 -0
  21. {krx_hj3415-2.2.3/src/krx_hj3415/provider → krx_hj3415-2.3.0/src/krx_hj3415/app/adapters}/__init__.py +0 -0
  22. {krx_hj3415-2.2.3/src/krx_hj3415/usecases → krx_hj3415-2.3.0/src/krx_hj3415/app/domain}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: krx-hj3415
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: KRX300 code scraper
5
5
  Keywords: example,demo
6
6
  Author-email: Hyungjin Kim <hj3415@gmail.com>
@@ -17,7 +17,7 @@ Requires-Dist: xlrd>=2.0.2
17
17
  Requires-Dist: typer>=0.21.1
18
18
  Requires-Dist: db2-hj3415
19
19
  Requires-Dist: contracts-hj3415
20
- Requires-Dist: domain-hj3415
20
+ Requires-Dist: common-hj3415
21
21
 
22
22
  # krx-hj3415
23
23
 
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "krx-hj3415" # PyPI 이름 (하이픈 허용)
7
- version = "2.2.3"
7
+ version = "2.3.0"
8
8
  description = "KRX300 code scraper"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "typer>=0.21.1",
26
26
  "db2-hj3415",
27
27
  "contracts-hj3415",
28
- "domain-hj3415",
28
+ "common-hj3415",
29
29
  ]
30
30
 
31
31
  [tool.flit.module]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Sequence
4
+
5
+ from pymongo.asynchronous.database import AsyncDatabase
6
+
7
+ from db2_hj3415.nfs.repo import delete_codes_from_nfs
8
+ from krx_hj3415.app.ports.nfs_cleaner import NfsCleanerPort
9
+
10
+
11
+ class NfsCleanerDb2(NfsCleanerPort):
12
+ def __init__(self, db: AsyncDatabase):
13
+ self._db = db
14
+
15
+ async def delete_codes(self, *, codes: Sequence[str]) -> dict[str, int]:
16
+ return await delete_codes_from_nfs(self._db, codes=codes, endpoint=None)
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from pymongo.asynchronous.database import AsyncDatabase
4
+
5
+ from contracts_hj3415.universe.dto import UniverseDTO
6
+ from contracts_hj3415.universe.types import Universe
7
+
8
+ from db2_hj3415.universe import repo as urepo
9
+ from db2_hj3415.universe import mappers as umap
10
+
11
+ from krx_hj3415.app.ports.universe_store import UniverseStorePort
12
+
13
+
14
+ class UniverseStoreDb2(UniverseStorePort):
15
+ def __init__(self, db: AsyncDatabase):
16
+ self._db = db
17
+
18
+ async def get_latest(self, *, universe: Universe) -> UniverseDTO | None:
19
+ doc = await urepo.get_latest(self._db, universe=universe)
20
+ if not doc:
21
+ return None
22
+ return umap.latest_doc_to_dto(doc)
23
+
24
+ async def upsert_latest(self, *, dto: UniverseDTO) -> None:
25
+ doc = umap.dto_to_latest_doc(dto)
26
+ await urepo.upsert_latest_doc(self._db, doc=doc)
27
+
28
+ async def insert_snapshot(self, *, dto: UniverseDTO) -> None:
29
+ doc = umap.dto_to_snapshot_doc(dto)
30
+ await urepo.insert_snapshot_doc(self._db, doc=doc)
@@ -1,9 +1,9 @@
1
- # krx_hj3415/domain/diff.py
1
+ # krx_hj3415/app/domain/diff.py
2
2
  from __future__ import annotations
3
3
 
4
4
  from datetime import datetime
5
5
  from typing import Iterable
6
- from .types import UniverseDiff, CodeItem
6
+ from .types import UniverseDiff, CodeItem, UniverseEnum
7
7
 
8
8
 
9
9
  def _to_code_map(items: Iterable[CodeItem]) -> dict[str, CodeItem]:
@@ -12,7 +12,7 @@ def _to_code_map(items: Iterable[CodeItem]) -> dict[str, CodeItem]:
12
12
 
13
13
  def diff_universe(
14
14
  *,
15
- universe: str,
15
+ universe: UniverseEnum,
16
16
  asof: datetime,
17
17
  new_items: list[CodeItem],
18
18
  old_items: list[CodeItem],
@@ -1,21 +1,27 @@
1
- # krx_hj3415/domain/types.py
1
+ # krx_hj3415/app/domain/types.py
2
2
  from __future__ import annotations
3
3
 
4
4
  from dataclasses import dataclass
5
5
  from datetime import datetime
6
+ from enum import StrEnum
6
7
 
8
+ class UniverseEnum(StrEnum):
9
+ KRX300 = "KRX300"
10
+
11
+ class MarketEnum(StrEnum):
12
+ KRX = "KRX"
7
13
 
8
14
  @dataclass(frozen=True)
9
15
  class CodeItem:
10
16
  code: str
11
17
  name: str
12
18
  asof: datetime
13
- market: str | None = None # 확장 대비(예: KRX/KOSPI/KOSDAQ/NYSE/NASDAQ 등)
19
+ market: MarketEnum
14
20
 
15
21
 
16
22
  @dataclass(frozen=True)
17
23
  class UniverseDiff:
18
- universe: str
24
+ universe: UniverseEnum
19
25
  asof: datetime
20
26
  added: list[CodeItem]
21
27
  removed: list[CodeItem]
File without changes
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol, Sequence
4
+
5
+
6
+ class NfsCleanerPort(Protocol):
7
+ async def delete_codes(self, *, codes: Sequence[str]) -> dict[str, int]:
8
+ ...
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from contracts_hj3415.universe.dto import UniverseDTO
6
+ from contracts_hj3415.universe.types import Universe
7
+
8
+
9
+ class UniverseStorePort(Protocol):
10
+ async def get_latest(self, *, universe: Universe) -> UniverseDTO | None:
11
+ ...
12
+
13
+ async def upsert_latest(self, *, dto: UniverseDTO) -> None:
14
+ ...
15
+
16
+ async def insert_snapshot(self, *, dto: UniverseDTO) -> None:
17
+ ...
@@ -1,4 +1,4 @@
1
- # krx_hj3415/provider/krx300_samsungfund_excel.py
1
+ # krx_hj3415/app/provider/krx300_samsungfund_excel.py
2
2
  from __future__ import annotations
3
3
 
4
4
  import random
@@ -8,9 +8,8 @@ import requests
8
8
  from datetime import datetime, timedelta, timezone
9
9
  from io import BytesIO
10
10
 
11
- from domain_hj3415.common.time import utcnow
12
- from krx_hj3415.domain.types import CodeItem
13
-
11
+ from common_hj3415.utils.time import utcnow
12
+ from krx_hj3415.app.domain.types import CodeItem, MarketEnum
14
13
 
15
14
  FUND_ID = "2ETFA4"
16
15
  BASE_URL = "https://www.samsungfund.com/excel_pdf.do"
@@ -55,6 +54,7 @@ def download_excel_bytes(*, max_days: int = 15) -> tuple[bytes, datetime, str]:
55
54
 
56
55
 
57
56
  def parse_krx300_items(excel_bytes: bytes, *, asof: datetime) -> list[CodeItem]:
57
+ market = MarketEnum.KRX
58
58
  # 네가 쓰던 조건: usecols B:I, skiprows 2
59
59
  df = pd.read_excel(BytesIO(excel_bytes), usecols="B:I", skiprows=2) # type: ignore
60
60
 
@@ -71,7 +71,7 @@ def parse_krx300_items(excel_bytes: bytes, *, asof: datetime) -> list[CodeItem]:
71
71
 
72
72
  items: list[CodeItem] = []
73
73
  for code, name in zip(df["종목코드"].astype(str), df["종목명"].astype(str)):
74
- items.append(CodeItem(code=code, name=name, asof=asof))
74
+ items.append(CodeItem(code=code, name=name, asof=asof, market=market))
75
75
  return items
76
76
 
77
77
 
@@ -0,0 +1,118 @@
1
+ # krx_hj3415/app/usecases/sync_universe.py
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+ from typing import Iterable, cast
6
+
7
+ from common_hj3415.utils.time import utcnow
8
+
9
+ from contracts_hj3415.universe.dto import UniverseItemDTO, UniverseDTO
10
+ from contracts_hj3415.universe.types import Universe
11
+
12
+ from krx_hj3415.app.domain.types import CodeItem, UniverseDiff, UniverseEnum, MarketEnum
13
+ from krx_hj3415.app.domain.diff import diff_universe
14
+ from krx_hj3415.app.provider.krx300_samsungfund_excel import fetch_krx300_items
15
+
16
+ from krx_hj3415.app.ports.universe_store import UniverseStorePort
17
+ from krx_hj3415.app.ports.nfs_cleaner import NfsCleanerPort
18
+
19
+
20
+ async def refresh_krx300(*, max_days: int = 15) -> tuple[datetime, list[CodeItem]]:
21
+ return fetch_krx300_items(max_days=max_days)
22
+
23
+
24
+ def _dto_to_code_items(dto: UniverseDTO | None) -> list[CodeItem]:
25
+ if not dto:
26
+ return []
27
+
28
+ out: list[CodeItem] = []
29
+ for row in dto.get("items", []):
30
+ if not isinstance(row, dict):
31
+ continue
32
+ code = str(row.get("code") or "").strip()
33
+ if not code:
34
+ continue
35
+ name = str(row.get("name") or "").strip()
36
+ market = MarketEnum(row.get("market"))
37
+ # old_item의 asof는 diff 용도라면 dto["asof"]를 쓰는 게 더 자연스럽긴 함(선택)
38
+ out.append(CodeItem(code=code, name=name, asof=utcnow(), market=market))
39
+ return out
40
+
41
+
42
+ def _code_items_to_dtos(
43
+ items: Iterable[CodeItem],
44
+ *,
45
+ market: MarketEnum = MarketEnum.KRX,
46
+ ) -> list[UniverseItemDTO]:
47
+ """
48
+ krx 도메인 CodeItem 목록을 contracts UniverseItemDTO 목록으로 변환한다.
49
+
50
+ - CodeItem.code 는 필수
51
+ - CodeItem.name 은 비어있지 않을 때만 포함
52
+ - market 은 krx 정책(default: "KRX")에 따라 주입
53
+ """
54
+ out: list[UniverseItemDTO] = []
55
+
56
+ for it in items:
57
+ if it is None:
58
+ continue
59
+
60
+ code = (it.code or "").strip()
61
+ if not code:
62
+ continue
63
+
64
+ dto: UniverseItemDTO = {
65
+ "code": code,
66
+ "market": market.value,
67
+ }
68
+
69
+ if isinstance(it.name, str) and it.name.strip():
70
+ dto["name"] = it.name.strip()
71
+
72
+ out.append(dto)
73
+
74
+ return out
75
+
76
+
77
+ async def apply_removed(
78
+ cleaner: NfsCleanerPort,
79
+ *,
80
+ removed_codes: Iterable[str],
81
+ ) -> dict[str, int]:
82
+ codes = [str(c).strip() for c in removed_codes if c and str(c).strip()]
83
+ if not codes:
84
+ return {"latest_deleted": 0, "snapshots_deleted": 0}
85
+ return await cleaner.delete_codes(codes=codes)
86
+
87
+
88
+ async def run_sync(
89
+ universe_store: UniverseStorePort,
90
+ *,
91
+ universe: UniverseEnum = UniverseEnum.KRX300,
92
+ max_days: int = 15,
93
+ snapshot: bool = True,
94
+ ) -> UniverseDiff:
95
+ # 1) fetch
96
+ asof, new_items = await refresh_krx300(max_days=max_days)
97
+
98
+ # 2) load old (DTO)
99
+ old_dto = await universe_store.get_latest(universe=cast(Universe, universe.value))
100
+ old_items = _dto_to_code_items(old_dto)
101
+
102
+ # 3) diff
103
+ d = diff_universe(
104
+ universe=universe, asof=asof, new_items=new_items, old_items=old_items
105
+ )
106
+
107
+ # 4) save (DTO로 저장 요청)
108
+ dto: UniverseDTO = {
109
+ "universe": universe.value,
110
+ "asof": asof,
111
+ "source": "samsungfund",
112
+ "items": _code_items_to_dtos(new_items, market=MarketEnum.KRX),
113
+ }
114
+ await universe_store.upsert_latest(dto=dto)
115
+ if snapshot:
116
+ await universe_store.insert_snapshot(dto=dto)
117
+
118
+ return d
@@ -9,8 +9,14 @@ from db2_hj3415.settings import get_settings
9
9
  from db2_hj3415.universe.repo import ensure_indexes as ensure_indexes_universe
10
10
  from db2_hj3415.nfs.repo import ensure_indexes as ensure_indexes_nfs
11
11
 
12
- from krx_hj3415.domain.universe import UniverseKind
13
- from krx_hj3415.usecases.sync_universe import run_sync, apply_removed
12
+ from krx_hj3415.app.ports.nfs_cleaner import NfsCleanerPort
13
+ from krx_hj3415.app.ports.universe_store import UniverseStorePort
14
+
15
+ from krx_hj3415.app.adapters.universe_store_db2 import UniverseStoreDb2
16
+ from krx_hj3415.app.adapters.nfs_store_db2 import NfsCleanerDb2
17
+
18
+ from krx_hj3415.app.domain.types import UniverseEnum
19
+ from krx_hj3415.app.usecases.sync_universe import run_sync, apply_removed
14
20
 
15
21
  app = typer.Typer(no_args_is_help=True)
16
22
 
@@ -41,10 +47,16 @@ async def _mongo_bootstrap(db) -> None:
41
47
 
42
48
  @app.command()
43
49
  def sync(
44
- universe: str = typer.Argument("krx300", help="유니버스 이름 (현재: krx300)"),
45
- apply: bool = typer.Option(False, "--apply", help="removed를 nfs에서 실제 삭제까지 적용"),
46
- snapshot: bool = typer.Option(True, "--snapshot/--no-snapshot", help="universe snapshot 저장 여부"),
47
- max_days: int = typer.Option(15, "--max-days", help="최대 며칠 전까지 유효 엑셀 URL 탐색"),
50
+ universe: str = typer.Argument("KRX300", help="유니버스 이름 (현재: KRX300)"),
51
+ apply: bool = typer.Option(
52
+ False, "--apply", help="removed를 nfs에서 실제 삭제까지 적용"
53
+ ),
54
+ snapshot: bool = typer.Option(
55
+ True, "--snapshot/--no-snapshot", help="universe snapshot 저장 여부"
56
+ ),
57
+ max_days: int = typer.Option(
58
+ 15, "--max-days", help="최대 며칠 전까지 유효 엑셀 URL 탐색"
59
+ ),
48
60
  ):
49
61
  """
50
62
  1) 외부에서 유니버스 수집
@@ -59,11 +71,21 @@ def sync(
59
71
  try:
60
72
  await _mongo_bootstrap(db)
61
73
 
62
- d = await run_sync(db, universe=UniverseKind(universe), max_days=max_days, snapshot=snapshot)
74
+ store: UniverseStorePort = UniverseStoreDb2(db)
75
+ cleaner: NfsCleanerPort = NfsCleanerDb2(db)
76
+
77
+ d = await run_sync(
78
+ store,
79
+ universe=UniverseEnum(universe),
80
+ max_days=max_days,
81
+ snapshot=snapshot,
82
+ )
63
83
 
64
84
  typer.echo(f"\n=== UNIVERSE SYNC: {d.universe} ===")
65
85
  typer.echo(f"asof: {d.asof.isoformat()}")
66
- typer.echo(f"added: {len(d.added)}, removed: {len(d.removed)}, kept: {d.kept_count}")
86
+ typer.echo(
87
+ f"added: {len(d.added)}, removed: {len(d.removed)}, kept: {d.kept_count}"
88
+ )
67
89
 
68
90
  if d.added:
69
91
  typer.echo("\n[ADDED]")
@@ -81,7 +103,7 @@ def sync(
81
103
 
82
104
  if apply and d.removed:
83
105
  typer.echo("\n=== APPLY REMOVED TO NFS ===")
84
- r = await apply_removed(db, removed_codes=d.removed_codes)
106
+ r = await apply_removed(cleaner, removed_codes=d.removed_codes)
85
107
  typer.echo(
86
108
  f"latest_deleted={r.get('latest_deleted', 0)}, "
87
109
  f"snapshots_deleted={r.get('snapshots_deleted', 0)}"
@@ -96,4 +118,4 @@ def sync(
96
118
 
97
119
 
98
120
  if __name__ == "__main__":
99
- app()
121
+ app()
@@ -1,8 +0,0 @@
1
- # krx300_hj3415/domain/universe.py
2
- from __future__ import annotations
3
-
4
- from enum import StrEnum
5
-
6
- class UniverseKind(StrEnum):
7
- KRX300 = "krx300"
8
-
@@ -1,136 +0,0 @@
1
- # krx_hj3415/usecases/sync_universe.py
2
- from __future__ import annotations
3
-
4
- from datetime import datetime
5
- from typing import Any, Iterable, cast
6
- from pymongo.asynchronous.database import AsyncDatabase
7
-
8
- from domain_hj3415.common.time import utcnow
9
-
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
-
17
- from contracts_hj3415.universe.dto import UniverseItemDTO, UniversePayloadDTO
18
- from contracts_hj3415.universe.types import UniverseNames
19
-
20
- from krx_hj3415.domain.types import CodeItem, UniverseDiff
21
- from krx_hj3415.domain.diff import diff_universe
22
- from krx_hj3415.domain.universe import UniverseKind
23
- from krx_hj3415.provider.krx300_samsungfund_excel import fetch_krx300_items
24
-
25
-
26
- def _payload_dto_to_items(dto: UniversePayloadDTO) -> list[CodeItem]:
27
- if dto is None:
28
- return []
29
-
30
- data = dto["items"]
31
-
32
- out: list[CodeItem] = []
33
- for row in data:
34
- if not isinstance(row, dict):
35
- continue
36
- code = str(row.get("code") or "").strip()
37
- if not code:
38
- continue
39
- name = str(row.get("name") or "").strip()
40
- market = row.get("market")
41
- asof = utcnow()
42
- out.append(CodeItem(code=code, name=name, asof=asof, market=market))
43
- return out
44
-
45
-
46
- async def refresh_krx300(*, max_days: int = 15) -> tuple[datetime, list[CodeItem]]:
47
- # 현재는 KRX300만 구현. 다른 universe도 늘리면 여기서 분기
48
- return fetch_krx300_items(max_days=max_days)
49
-
50
-
51
- def to_universe_item_dtos(
52
- items: Iterable[Any], *, market: str = "KRX"
53
- ) -> list[UniverseItemDTO]:
54
- out: list[UniverseItemDTO] = []
55
- for it in items:
56
- if it is None:
57
- continue
58
-
59
- if isinstance(it, dict):
60
- code = (it.get("code") or "").strip()
61
- name = it.get("name")
62
- else:
63
- code = (getattr(it, "code", "") or "").strip()
64
- name = getattr(it, "name", None)
65
-
66
- if not code:
67
- continue
68
-
69
- dto: UniverseItemDTO = {"code": code, "market": market}
70
- if isinstance(name, str) and name.strip():
71
- dto["name"] = name.strip()
72
-
73
- out.append(dto)
74
- return out
75
-
76
-
77
- async def run_sync(
78
- db: AsyncDatabase,
79
- *,
80
- universe: UniverseKind = UniverseKind.KRX300,
81
- max_days: int = 15,
82
- snapshot: bool = True,
83
- ) -> UniverseDiff:
84
- """
85
- 1) 외부에서 최신 유니버스 수집
86
- 2) DB에서 이전 latest 조회
87
- 3) diff 계산
88
- 4) latest upsert + (선택) snapshots insert
89
- """
90
- # --- 1) fetch ---
91
- asof, new_items = await refresh_krx300(max_days=max_days)
92
-
93
- # --- 2) load old ---
94
- old_payload_dto = await get_universe_latest(
95
- db, universe=cast(UniverseNames, universe.value)
96
- )
97
- old_items = _payload_dto_to_items(old_payload_dto)
98
-
99
- # --- 3) diff ---
100
- d = diff_universe(
101
- universe=universe.value, asof=asof, new_items=new_items, old_items=old_items
102
- )
103
-
104
- # --- 4) save ---
105
- await upsert_universe_latest(
106
- db,
107
- universe=cast(UniverseNames, universe.value),
108
- items=to_universe_item_dtos(new_items, market="KRX"),
109
- asof=asof,
110
- source="samsungfund",
111
- )
112
- if snapshot:
113
- await insert_universe_snapshot(
114
- db,
115
- universe=cast(UniverseNames, universe.value),
116
- items=to_universe_item_dtos(new_items, market="KRX"),
117
- asof=asof,
118
- source="samsungfund",
119
- )
120
-
121
- return d
122
-
123
-
124
- async def apply_removed(
125
- db: AsyncDatabase,
126
- *,
127
- removed_codes: Iterable[str],
128
- ) -> dict[str, int]:
129
- """
130
- removed codes를 nfs(latest/snapshots)에서 모두 삭제.
131
- """
132
- codes = [str(c).strip() for c in removed_codes if c and str(c).strip()]
133
- if not codes:
134
- return {"latest_deleted": 0, "snapshots_deleted": 0}
135
-
136
- return await delete_codes_from_nfs(db, codes=codes, endpoint=None)
File without changes
File without changes