hyean 0.4.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.
hyean-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyean
3
+ Version: 0.4.0
4
+ Summary: Korean financial data SDK — DART 5소스 교차검증, LLM-ready
5
+ Author-email: Hyean <support@hyean.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://hyean.io
8
+ Project-URL: Documentation, https://hyean.io/docs
9
+ Project-URL: Repository, https://github.com/twiw49/hyean
10
+ Keywords: korea,finance,dart,financial-data,xbrl,valuation,llm
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Office/Business :: Financial :: Investment
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: pandas>=2.0
21
+ Requires-Dist: requests>=2.28
22
+ Provides-Extra: parquet
23
+ Requires-Dist: pyarrow>=12.0; extra == "parquet"
24
+ Provides-Extra: server
25
+ Requires-Dist: fastapi>=0.115.0; extra == "server"
26
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == "server"
27
+ Requires-Dist: pydantic>=2.7.0; extra == "server"
28
+ Requires-Dist: pydantic-settings>=2.3.0; extra == "server"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: httpx; extra == "dev"
32
+
33
+ # hyean — Korean financial data SDK
34
+
35
+ DART 원문 기반 **5소스 교차검증** 한국 상장기업 재무 데이터. 모든 수치에 confidence 등급과 원문 출처(report_url + anchor)가 따라옵니다.
36
+
37
+ ```bash
38
+ pip install git+https://github.com/twiw49/hyean-sdk.git # 또는: pip install hyean
39
+ ```
40
+
41
+ ```python
42
+ import hyean as hy
43
+
44
+ hy.set_api_key("hyn_live_xxxx") # 또는 export HYEAN_API_KEY=...
45
+ # base_url 기본값 = https://api.hyean.io (생략 가능)
46
+
47
+ # 재무제표 → pandas DataFrame (식별자: 종목코드 / corp_code / 기업명)
48
+ bs = hy.load_balance("삼성전자")
49
+ is_ = hy.load_income("005930", min_confidence="high") # 품질 필터
50
+ cf = hy.load_cashflow("00126380")
51
+ ```
52
+
53
+ ### dart-fss와 다른 점 — point-in-time(정정 소급반영)
54
+
55
+ ```python
56
+ # 특정 시점 기준으로 본 재무 — 이후 정정공시를 반영/미반영해 백테스트 무결성 확보
57
+ bs_then = hy.load_balance("005930", as_of="2023-03-31")
58
+ ```
59
+
60
+ `as_of`는 survivorship bias와 미반영 정정으로 백테스트가 틀어지는 문제를 막습니다 — 무료 오픈소스가 싸게 제공하지 못하는 지점입니다.
61
+
62
+ ### 원문 추적
63
+
64
+ ```python
65
+ bs = hy.load_balance("005930")
66
+ url = hy.source_url(bs, "BS_AST_TOT", "2024") # 이 숫자의 DART 원문 위치
67
+ ```
68
+
69
+ ### CompanyHub — 한 기업, 여러 관점
70
+
71
+ ```python
72
+ hub = hy.CompanyHub("005930")
73
+ hub.balance, hub.income, hub.cashflow, hub.audit, hub.profile, hub.risk
74
+ ```
75
+
76
+ ---
77
+
78
+ API 키 발급: https://hyean.io · 문서: https://api.hyean.io/docs · 가격: https://hyean.io/pricing
hyean-0.4.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # hyean — Korean financial data SDK
2
+
3
+ DART 원문 기반 **5소스 교차검증** 한국 상장기업 재무 데이터. 모든 수치에 confidence 등급과 원문 출처(report_url + anchor)가 따라옵니다.
4
+
5
+ ```bash
6
+ pip install git+https://github.com/twiw49/hyean-sdk.git # 또는: pip install hyean
7
+ ```
8
+
9
+ ```python
10
+ import hyean as hy
11
+
12
+ hy.set_api_key("hyn_live_xxxx") # 또는 export HYEAN_API_KEY=...
13
+ # base_url 기본값 = https://api.hyean.io (생략 가능)
14
+
15
+ # 재무제표 → pandas DataFrame (식별자: 종목코드 / corp_code / 기업명)
16
+ bs = hy.load_balance("삼성전자")
17
+ is_ = hy.load_income("005930", min_confidence="high") # 품질 필터
18
+ cf = hy.load_cashflow("00126380")
19
+ ```
20
+
21
+ ### dart-fss와 다른 점 — point-in-time(정정 소급반영)
22
+
23
+ ```python
24
+ # 특정 시점 기준으로 본 재무 — 이후 정정공시를 반영/미반영해 백테스트 무결성 확보
25
+ bs_then = hy.load_balance("005930", as_of="2023-03-31")
26
+ ```
27
+
28
+ `as_of`는 survivorship bias와 미반영 정정으로 백테스트가 틀어지는 문제를 막습니다 — 무료 오픈소스가 싸게 제공하지 못하는 지점입니다.
29
+
30
+ ### 원문 추적
31
+
32
+ ```python
33
+ bs = hy.load_balance("005930")
34
+ url = hy.source_url(bs, "BS_AST_TOT", "2024") # 이 숫자의 DART 원문 위치
35
+ ```
36
+
37
+ ### CompanyHub — 한 기업, 여러 관점
38
+
39
+ ```python
40
+ hub = hy.CompanyHub("005930")
41
+ hub.balance, hub.income, hub.cashflow, hub.audit, hub.profile, hub.risk
42
+ ```
43
+
44
+ ---
45
+
46
+ API 키 발급: https://hyean.io · 문서: https://api.hyean.io/docs · 가격: https://hyean.io/pricing
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyean
3
+ Version: 0.4.0
4
+ Summary: Korean financial data SDK — DART 5소스 교차검증, LLM-ready
5
+ Author-email: Hyean <support@hyean.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://hyean.io
8
+ Project-URL: Documentation, https://hyean.io/docs
9
+ Project-URL: Repository, https://github.com/twiw49/hyean
10
+ Keywords: korea,finance,dart,financial-data,xbrl,valuation,llm
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Office/Business :: Financial :: Investment
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: pandas>=2.0
21
+ Requires-Dist: requests>=2.28
22
+ Provides-Extra: parquet
23
+ Requires-Dist: pyarrow>=12.0; extra == "parquet"
24
+ Provides-Extra: server
25
+ Requires-Dist: fastapi>=0.115.0; extra == "server"
26
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == "server"
27
+ Requires-Dist: pydantic>=2.7.0; extra == "server"
28
+ Requires-Dist: pydantic-settings>=2.3.0; extra == "server"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: httpx; extra == "dev"
32
+
33
+ # hyean — Korean financial data SDK
34
+
35
+ DART 원문 기반 **5소스 교차검증** 한국 상장기업 재무 데이터. 모든 수치에 confidence 등급과 원문 출처(report_url + anchor)가 따라옵니다.
36
+
37
+ ```bash
38
+ pip install git+https://github.com/twiw49/hyean-sdk.git # 또는: pip install hyean
39
+ ```
40
+
41
+ ```python
42
+ import hyean as hy
43
+
44
+ hy.set_api_key("hyn_live_xxxx") # 또는 export HYEAN_API_KEY=...
45
+ # base_url 기본값 = https://api.hyean.io (생략 가능)
46
+
47
+ # 재무제표 → pandas DataFrame (식별자: 종목코드 / corp_code / 기업명)
48
+ bs = hy.load_balance("삼성전자")
49
+ is_ = hy.load_income("005930", min_confidence="high") # 품질 필터
50
+ cf = hy.load_cashflow("00126380")
51
+ ```
52
+
53
+ ### dart-fss와 다른 점 — point-in-time(정정 소급반영)
54
+
55
+ ```python
56
+ # 특정 시점 기준으로 본 재무 — 이후 정정공시를 반영/미반영해 백테스트 무결성 확보
57
+ bs_then = hy.load_balance("005930", as_of="2023-03-31")
58
+ ```
59
+
60
+ `as_of`는 survivorship bias와 미반영 정정으로 백테스트가 틀어지는 문제를 막습니다 — 무료 오픈소스가 싸게 제공하지 못하는 지점입니다.
61
+
62
+ ### 원문 추적
63
+
64
+ ```python
65
+ bs = hy.load_balance("005930")
66
+ url = hy.source_url(bs, "BS_AST_TOT", "2024") # 이 숫자의 DART 원문 위치
67
+ ```
68
+
69
+ ### CompanyHub — 한 기업, 여러 관점
70
+
71
+ ```python
72
+ hub = hy.CompanyHub("005930")
73
+ hub.balance, hub.income, hub.cashflow, hub.audit, hub.profile, hub.risk
74
+ ```
75
+
76
+ ---
77
+
78
+ API 키 발급: https://hyean.io · 문서: https://api.hyean.io/docs · 가격: https://hyean.io/pricing
@@ -0,0 +1,38 @@
1
+ README.md
2
+ pyproject.toml
3
+ hyean.egg-info/PKG-INFO
4
+ hyean.egg-info/SOURCES.txt
5
+ hyean.egg-info/dependency_links.txt
6
+ hyean.egg-info/requires.txt
7
+ hyean.egg-info/top_level.txt
8
+ sdk/__init__.py
9
+ sdk/audit.py
10
+ sdk/benchmark.py
11
+ sdk/cache.py
12
+ sdk/client.py
13
+ sdk/companies.py
14
+ sdk/config.py
15
+ sdk/context.py
16
+ sdk/coverage.py
17
+ sdk/deep.py
18
+ sdk/discover.py
19
+ sdk/enhanced.py
20
+ sdk/events.py
21
+ sdk/exceptions.py
22
+ sdk/filings.py
23
+ sdk/financials.py
24
+ sdk/grades.py
25
+ sdk/groups.py
26
+ sdk/hub.py
27
+ sdk/index.py
28
+ sdk/macro.py
29
+ sdk/meta.py
30
+ sdk/names.py
31
+ sdk/prices.py
32
+ sdk/profile.py
33
+ sdk/segments.py
34
+ sdk/source.py
35
+ sdk/utils.py
36
+ sdk/valuation.py
37
+ tests/test_reference.py
38
+ tests/test_smoke.py
@@ -0,0 +1,15 @@
1
+ pandas>=2.0
2
+ requests>=2.28
3
+
4
+ [dev]
5
+ pytest
6
+ httpx
7
+
8
+ [parquet]
9
+ pyarrow>=12.0
10
+
11
+ [server]
12
+ fastapi>=0.115.0
13
+ uvicorn[standard]>=0.30.0
14
+ pydantic>=2.7.0
15
+ pydantic-settings>=2.3.0
@@ -0,0 +1 @@
1
+ hyean
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hyean"
7
+ version = "0.4.0"
8
+ description = "Korean financial data SDK — DART 5소스 교차검증, LLM-ready"
9
+ license = {text = "MIT"}
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Hyean", email = "support@hyean.io" }]
13
+ keywords = ["korea", "finance", "dart", "financial-data", "xbrl", "valuation", "llm"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Financial and Insurance Industry",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Office/Business :: Financial :: Investment",
22
+ ]
23
+ dependencies = [
24
+ "pandas>=2.0",
25
+ "requests>=2.28",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://hyean.io"
30
+ Documentation = "https://hyean.io/docs"
31
+ Repository = "https://github.com/twiw49/hyean"
32
+
33
+ [project.optional-dependencies]
34
+ parquet = ["pyarrow>=12.0"]
35
+ server = [
36
+ "fastapi>=0.115.0",
37
+ "uvicorn[standard]>=0.30.0",
38
+ "pydantic>=2.7.0",
39
+ "pydantic-settings>=2.3.0",
40
+ ]
41
+ dev = ["pytest", "httpx"]
42
+
43
+ # pip install hyean → sdk/ 만 설치됨
44
+ # pip install hyean[server] → 서버 의존성 포함
45
+ [tool.setuptools]
46
+ packages = ["hyean"]
47
+
48
+ [tool.setuptools.package-dir]
49
+ hyean = "sdk"
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ # 스모크 테스트는 DB가 떠 있어야 의미 있음 (데이터 의존 테스트는 db_up fixture로 자동 skip)
@@ -0,0 +1,115 @@
1
+ """
2
+ Hyean — Korean financial data SDK
3
+ ===================================
4
+ DART 원문 기반, 5소스 교차검증 한국 상장기업 재무 데이터.
5
+
6
+ Quick start:
7
+ import hyean as hy
8
+
9
+ hy.set_api_key("hyn_live_xxxx")
10
+ hy.set_base_url("https://api.hyean.io/v1")
11
+
12
+ # 스마트 식별자 — corp_code, stock_code, 기업명 모두 가능
13
+ bs = hy.load_balance("005930")
14
+ bs = hy.load_balance("삼성전자")
15
+ bs = hy.load_balance("00126380")
16
+
17
+ # 복수 기업
18
+ bs = hy.load_balance(["005930", "000660"])
19
+
20
+ # 품질 필터
21
+ bs = hy.load_balance("005930", min_confidence="high")
22
+
23
+ # 원문 추적
24
+ url = hy.source_url(bs, "BS_AST_TOT", "2024")
25
+
26
+ # CompanyHub
27
+ hub = hy.CompanyHub("005930")
28
+ hub.balance, hub.income, hub.audit
29
+ """
30
+
31
+ __version__ = "0.4.0"
32
+
33
+ # ── Config ──
34
+ from .config import (
35
+ set_api_key, get_api_key, load_api_key,
36
+ set_base_url, get_base_url,
37
+ set_data_dir, get_data_dir,
38
+ set_cache, get_cache_format, get_refresh_days,
39
+ )
40
+
41
+ # ── Companies ──
42
+ from .companies import load_companies, search_companies, company_detail, load_company, resolve
43
+
44
+ # ── Discover (AI 카탈로그) ──
45
+ from .discover import discover
46
+
47
+ # ── Financials (core) ──
48
+ from .financials import (
49
+ load_financials,
50
+ load_balance, load_income, load_cashflow,
51
+ load_derived,
52
+ )
53
+
54
+ # ── Events & Anomalies ──
55
+ from .events import load_events, search_events, load_anomalies
56
+
57
+ # ── Groups / Rank / Grades ──
58
+ from .groups import load_groups, load_rank
59
+ from .grades import load_grades
60
+
61
+ # ── Profile / Risk / Quality / Notes / Insight ──
62
+ from .profile import (
63
+ load_profile, load_risk, load_quality,
64
+ load_notes, load_note_elements, load_insight_context,
65
+ )
66
+
67
+ # ── Segments ──
68
+ from .segments import load_segments, load_segment_axes
69
+
70
+ # ── Insight S1 context ──
71
+ from .context import (
72
+ load_context_categories, load_context, load_context_available, search_context,
73
+ )
74
+
75
+ # ── Audit / Coverage / Benchmark / Deep ──
76
+ from .audit import load_audit
77
+ from .coverage import load_coverage
78
+ from .benchmark import load_benchmark
79
+ from .deep import load_deep
80
+
81
+ # ── Valuation ──
82
+ from .valuation import load_valuation
83
+
84
+ # ── Filings ──
85
+ from .filings import load_filings, load_filing
86
+
87
+ # ── Prices ──
88
+ from .prices import load_prices
89
+
90
+ # ── Enhanced (AI Agent 최적화) ──
91
+ from .enhanced import (
92
+ load_summary, load_price, load_price_stats,
93
+ load_quarterly, load_peers, load_prices_bulk, screening,
94
+ )
95
+
96
+ # ── Macro (ECOS) ──
97
+ from .macro import load_macro, load_macro_rates, load_macro_indicators, load_macro_context
98
+
99
+ # ── KRX Index ──
100
+ from .index import load_index_list, load_index_timeseries, load_index_constituents
101
+
102
+ # ── Source provenance ──
103
+ from .source import load_source_detail, load_source_items
104
+
105
+ # ── Meta ──
106
+ from .meta import load_items, load_stats
107
+
108
+ # ── Utils ──
109
+ from .utils import pivot_financials, source_url, source_detail
110
+
111
+ # ── Hub ──
112
+ from .hub import CompanyHub
113
+
114
+ # ── Exceptions ──
115
+ from .exceptions import HyeanError, APIError, NotFoundError, ConfigError
@@ -0,0 +1,17 @@
1
+ """Audit opinions, going concern, KAM"""
2
+ from .client import get_client
3
+ from .companies import resolve
4
+
5
+
6
+ def load_audit(code: str | None = None, *, corp_code: str | None = None, stock_code: str | None = None) -> dict:
7
+ """
8
+ Load audit opinions, going concern warnings, and KAM.
9
+
10
+ Args:
11
+ code: Smart identifier (corp_code/stock_code/name)
12
+
13
+ Returns:
14
+ Dict with keys: corp_code, total, has_going_concern, reports[]
15
+ """
16
+ cc = resolve(code or corp_code or stock_code)
17
+ return get_client().get(f"/api/companies/{cc}/audit")
@@ -0,0 +1,32 @@
1
+ """Sector benchmark comparison"""
2
+ from .client import get_client
3
+ from .companies import resolve
4
+
5
+
6
+ def load_benchmark(
7
+ code: str | None = None,
8
+ *,
9
+ corp_code: str | None = None,
10
+ stock_code: str | None = None,
11
+ consol: int = 1,
12
+ group_by: str | None = None,
13
+ group_code: str | None = None,
14
+ ) -> dict:
15
+ """
16
+ Compare company metrics against group benchmarks.
17
+
18
+ Args:
19
+ code: Smart identifier (corp_code/stock_code/name)
20
+ group_by: Comparison dimension (industry/krx_index/market/size/ecos_sector)
21
+ group_code: Specific group within dimension (auto-resolved if omitted)
22
+
23
+ Returns:
24
+ Dict with keys: corp_name, sector_code, items[]
25
+ """
26
+ cc = resolve(code or corp_code or stock_code)
27
+ params: dict = {"consol": consol}
28
+ if group_by:
29
+ params["group_by"] = group_by
30
+ if group_code:
31
+ params["group_code"] = group_code
32
+ return get_client().get(f"/api/companies/{cc}/benchmark", params)
@@ -0,0 +1,89 @@
1
+ """
2
+ Caching decorator for Hyean SDK
3
+ =================================
4
+ Caches DataFrame results to local disk.
5
+ """
6
+ import time
7
+ import hashlib
8
+ import functools
9
+ from pathlib import Path
10
+
11
+ import pandas as pd
12
+
13
+ from .config import get_data_dir, get_cache_format, get_refresh_days
14
+
15
+
16
+ def _cache_path(name: str, fmt: str) -> Path | None:
17
+ data_dir = get_data_dir()
18
+ if data_dir is None:
19
+ return None
20
+ ext = {"pickle": ".pkl", "parquet": ".parquet"}.get(fmt, ".pkl")
21
+ return data_dir / "cache" / f"{name}{ext}"
22
+
23
+
24
+ def _file_age_days(path: Path) -> float:
25
+ if not path.exists():
26
+ return float("inf")
27
+ return (time.time() - path.stat().st_mtime) / 86400
28
+
29
+
30
+ def _save_df(df: pd.DataFrame, path: Path, fmt: str):
31
+ path.parent.mkdir(parents=True, exist_ok=True)
32
+ if fmt == "parquet":
33
+ df.to_parquet(path)
34
+ else:
35
+ df.to_pickle(path)
36
+
37
+
38
+ def _load_df(path: Path, fmt: str) -> pd.DataFrame:
39
+ if fmt == "parquet":
40
+ return pd.read_parquet(path)
41
+ else:
42
+ return pd.read_pickle(path)
43
+
44
+
45
+ def cache(name_template: str, refresh_days: int | None = None):
46
+ """
47
+ Decorator that caches DataFrame results to disk.
48
+
49
+ Args:
50
+ name_template: Cache file name template with {param} placeholders.
51
+ e.g. "balance_{corp_code}_{consol}"
52
+ refresh_days: Override global refresh_days setting.
53
+
54
+ Usage:
55
+ @cache("balance_{corp_code}_{consol}")
56
+ def load_balance(corp_code, consol=1):
57
+ ...
58
+ """
59
+ def decorator(func):
60
+ @functools.wraps(func)
61
+ def wrapper(*args, **kwargs):
62
+ # Build cache key from function args
63
+ import inspect
64
+ sig = inspect.signature(func)
65
+ bound = sig.bind(*args, **kwargs)
66
+ bound.apply_defaults()
67
+
68
+ try:
69
+ cache_name = name_template.format(**bound.arguments)
70
+ except (KeyError, IndexError):
71
+ # Can't build cache name, skip caching
72
+ return func(*args, **kwargs)
73
+
74
+ fmt = get_cache_format()
75
+ path = _cache_path(cache_name, fmt)
76
+
77
+ if path is not None:
78
+ max_age = refresh_days if refresh_days is not None else get_refresh_days()
79
+ if _file_age_days(path) < max_age:
80
+ return _load_df(path, fmt)
81
+
82
+ result = func(*args, **kwargs)
83
+
84
+ if path is not None and isinstance(result, pd.DataFrame) and not result.empty:
85
+ _save_df(result, path, fmt)
86
+
87
+ return result
88
+ return wrapper
89
+ return decorator
@@ -0,0 +1,97 @@
1
+ """
2
+ HTTP client for Hyean API
3
+ ==========================
4
+ Handles requests, retries, error mapping.
5
+ """
6
+ import requests
7
+ from urllib.parse import urljoin
8
+
9
+ from .config import get_base_url, get_api_key
10
+ from .exceptions import APIError, NotFoundError, HyeanError
11
+
12
+
13
+ class HyeanClient:
14
+ """HTTP client with retry and error handling"""
15
+
16
+ def __init__(self, base_url: str | None = None, api_key: str | None = None):
17
+ self._base_url = base_url
18
+ self._api_key = api_key
19
+ self._session = requests.Session()
20
+ self._session.headers["User-Agent"] = "hyean-sdk/0.4.0"
21
+
22
+ @property
23
+ def base_url(self) -> str:
24
+ return self._base_url or get_base_url()
25
+
26
+ @property
27
+ def api_key(self) -> str | None:
28
+ return self._api_key or get_api_key()
29
+
30
+ def get(self, path: str, params: dict | None = None) -> dict:
31
+ """GET request, returns parsed JSON"""
32
+ url = urljoin(self.base_url + "/", path.lstrip("/"))
33
+ headers = {}
34
+ if self.api_key:
35
+ headers["Authorization"] = f"Bearer {self.api_key}"
36
+
37
+ try:
38
+ resp = self._session.get(url, params=params, headers=headers, timeout=30)
39
+ except requests.ConnectionError:
40
+ raise HyeanError(f"서버 연결 실패: {self.base_url}")
41
+ except requests.Timeout:
42
+ raise HyeanError("요청 타임아웃 (30초)")
43
+
44
+ if resp.status_code == 404:
45
+ raise NotFoundError(resp.text)
46
+ if resp.status_code >= 400:
47
+ detail = ""
48
+ try:
49
+ detail = resp.json().get("detail", resp.text)
50
+ except Exception:
51
+ detail = resp.text
52
+ raise APIError(resp.status_code, detail)
53
+
54
+ return resp.json()
55
+
56
+ def post(self, path: str, json_body: dict | None = None) -> dict:
57
+ """POST request, returns parsed JSON"""
58
+ url = urljoin(self.base_url + "/", path.lstrip("/"))
59
+ headers = {"Content-Type": "application/json"}
60
+ if self.api_key:
61
+ headers["Authorization"] = f"Bearer {self.api_key}"
62
+
63
+ try:
64
+ resp = self._session.post(url, json=json_body, headers=headers, timeout=60)
65
+ except requests.ConnectionError:
66
+ raise HyeanError(f"서버 연결 실패: {self.base_url}")
67
+ except requests.Timeout:
68
+ raise HyeanError("요청 타임아웃 (60초)")
69
+
70
+ if resp.status_code == 404:
71
+ raise NotFoundError(resp.text)
72
+ if resp.status_code >= 400:
73
+ detail = ""
74
+ try:
75
+ detail = resp.json().get("detail", resp.text)
76
+ except Exception:
77
+ detail = resp.text
78
+ raise APIError(resp.status_code, detail)
79
+
80
+ return resp.json()
81
+
82
+
83
+ # Module-level singleton
84
+ _client: HyeanClient | None = None
85
+
86
+
87
+ def get_client() -> HyeanClient:
88
+ global _client
89
+ if _client is None:
90
+ _client = HyeanClient()
91
+ return _client
92
+
93
+
94
+ def reset_client():
95
+ """Reset client (e.g. after changing base_url)"""
96
+ global _client
97
+ _client = None