Use user config directory for runtime state
This commit is contained in:
@@ -18,6 +18,10 @@ KIS_APPSECRET=your_appsecret_here
|
|||||||
# KIS_ACCESS_TOKEN_EXPIRED_AT=
|
# KIS_ACCESS_TOKEN_EXPIRED_AT=
|
||||||
# KIS_ACCESS_TOKEN_EXPIRES_IN=
|
# KIS_ACCESS_TOKEN_EXPIRES_IN=
|
||||||
|
|
||||||
|
# Runtime config file location:
|
||||||
|
# ~/.config/openstock/.env
|
||||||
|
# You can override the config root with OPENSTOCK_CONFIG_DIR.
|
||||||
|
|
||||||
# OpenDART 공시정보 API 키
|
# OpenDART 공시정보 API 키
|
||||||
# https://opendart.fss.or.kr/ 에서 발급
|
# https://opendart.fss.or.kr/ 에서 발급
|
||||||
OPENDART_API_KEY=your_opendart_api_key_here
|
OPENDART_API_KEY=your_opendart_api_key_here
|
||||||
|
|||||||
@@ -370,7 +370,7 @@
|
|||||||
|
|
||||||
| Key | Type | Meaning |
|
| Key | Type | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `namespace` | string | `.openstock` 아래 최상위 디렉터리 이름입니다. 예: `universe`, `opendart`. |
|
| `namespace` | string | cache root 아래 최상위 디렉터리 이름입니다. 예: `universe`, `opendart`. |
|
||||||
| `files` | number | namespace 안의 파일 수입니다. |
|
| `files` | number | namespace 안의 파일 수입니다. |
|
||||||
| `bytes` | number | namespace 안의 총 파일 크기입니다. |
|
| `bytes` | number | namespace 안의 총 파일 크기입니다. |
|
||||||
|
|
||||||
@@ -397,7 +397,23 @@
|
|||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
`.env` 파일에서 읽고, 일부 값은 명령 실행 중 갱신됩니다.
|
설정 파일은 기본적으로 `~/.config/openstock/.env`에서 읽고, 일부 값은 명령 실행 중 갱신됩니다. `OPENSTOCK_CONFIG_DIR`를 설정하면 이 루트 디렉터리를 바꿀 수 있습니다. 예를 들어 `OPENSTOCK_CONFIG_DIR=/tmp/openstock-test`이면 env 파일은 `/tmp/openstock-test/.env`, 캐시는 `/tmp/openstock-test/cache` 아래에 생성됩니다.
|
||||||
|
|
||||||
|
바이너리로 빌드해 어느 디렉터리에서 실행하더라도 기본 설정/캐시 위치는 동일합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/openstock
|
||||||
|
cp .env.example ~/.config/openstock/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
이전 개발 버전에서 프로젝트 루트의 `.env`와 `.openstock`을 사용했다면 다음처럼 옮길 수 있습니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/openstock
|
||||||
|
cp .env ~/.config/openstock/.env
|
||||||
|
mkdir -p ~/.config/openstock/cache
|
||||||
|
cp -R .openstock/* ~/.config/openstock/cache/
|
||||||
|
```
|
||||||
|
|
||||||
| Name | Required For | Direction | Description |
|
| Name | Required For | Direction | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
@@ -410,20 +426,20 @@
|
|||||||
| `KIS_ACCOUNT` | `account status`, `order buy`, `order sell`, `order status` | read | `CANO-ACNT_PRDT_CD` 형식 계좌입니다. 예: `12345678-01`. |
|
| `KIS_ACCOUNT` | `account status`, `order buy`, `order sell`, `order status` | read | `CANO-ACNT_PRDT_CD` 형식 계좌입니다. 예: `12345678-01`. |
|
||||||
| `OPENDART_API_KEY` | `dart *` | read | OpenDART API 인증키입니다. |
|
| `OPENDART_API_KEY` | `dart *` | read | OpenDART API 인증키입니다. |
|
||||||
|
|
||||||
`.env`는 `.gitignore` 대상입니다. 토큰과 계좌번호를 커밋하지 마십시오.
|
프로젝트 루트의 `.env`는 더 이상 런타임 기본 경로가 아닙니다. 토큰과 계좌번호를 커밋하지 마십시오.
|
||||||
|
|
||||||
## Cache and File IO
|
## Cache and File IO
|
||||||
|
|
||||||
로컬 캐시는 `.openstock` 아래에 생성되며 `.gitignore` 대상입니다.
|
로컬 캐시는 기본적으로 `~/.config/openstock/cache` 아래에 생성됩니다. `OPENSTOCK_CONFIG_DIR`를 사용하면 `{OPENSTOCK_CONFIG_DIR}/cache` 아래에 생성됩니다.
|
||||||
|
|
||||||
| Path | Writer | Reader | Retention | Description |
|
| Path | Writer | Reader | Retention | Description |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `.openstock/universe/kind/latest.json` | `universe sync` | `universe status/list/chunks/validate` | protected | KIND 상장법인목록 최신 normalized stock list입니다. |
|
| `~/.config/openstock/cache/universe/kind/latest.json` | `universe sync` | `universe status/list/chunks/validate` | protected | KIND 상장법인목록 최신 normalized stock list입니다. |
|
||||||
| `.openstock/universe/kind/meta.json` | `universe sync` | `universe status/*` | protected | universe cache metadata입니다. |
|
| `~/.config/openstock/cache/universe/kind/meta.json` | `universe sync` | `universe status/*` | protected | universe cache metadata입니다. |
|
||||||
| `.openstock/universe/kind/YYYY-MM-DD.json` | `universe sync` | audit/manual use | max 7 files or 25MB | 날짜별 universe snapshot입니다. |
|
| `~/.config/openstock/cache/universe/kind/YYYY-MM-DD.json` | `universe sync` | audit/manual use | max 7 files or 25MB | 날짜별 universe snapshot입니다. |
|
||||||
| `.openstock/opendart/corp_codes.json` | `dart sync`, `dart corp/disclosures/show` on cache miss | `dart corp/disclosures/show` | latest only | 상장사 `stock_code` to `corp_code` 매핑입니다. |
|
| `~/.config/openstock/cache/opendart/corp_codes.json` | `dart sync`, `dart corp/disclosures/show` on cache miss | `dart corp/disclosures/show` | latest only | 상장사 `stock_code` to `corp_code` 매핑입니다. |
|
||||||
| `.openstock/opendart/corp_codes_meta.json` | `dart sync` | `dart status` | latest only | OpenDART corp code cache metadata입니다. |
|
| `~/.config/openstock/cache/opendart/corp_codes_meta.json` | `dart sync` | `dart status` | latest only | OpenDART corp code cache metadata입니다. |
|
||||||
| `.openstock/opendart/documents/{rcept_no}.zip` | `dart document`, `dart show` | `dart document`, `dart show` | max 100 files or 200MB | 공시서류 원본 ZIP cache입니다. |
|
| `~/.config/openstock/cache/opendart/documents/{rcept_no}.zip` | `dart document`, `dart show` | `dart document`, `dart show` | max 100 files or 200MB | 공시서류 원본 ZIP cache입니다. |
|
||||||
|
|
||||||
캐시 상태와 정리는 다음 명령으로 수행합니다.
|
캐시 상태와 정리는 다음 명령으로 수행합니다.
|
||||||
|
|
||||||
@@ -466,10 +482,10 @@ KIS 실전 API 접근 토큰을 발급하거나 유효한 기존 토큰을 재
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Input | `--appkey`, `--appsecret`, `--force` |
|
| Input | `--appkey`, `--appsecret`, `--force` |
|
||||||
| Env read | `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCESS_TOKEN`, `KIS_ACCESS_TOKEN_EXPIRED_AT` |
|
| Env read | `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCESS_TOKEN`, `KIS_ACCESS_TOKEN_EXPIRED_AT` |
|
||||||
| Env write | `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCESS_TOKEN`, `KIS_ACCESS_TOKEN_EXPIRED_AT`, `KIS_TOKEN_TYPE`, `KIS_ACCESS_TOKEN_EXPIRES_IN` |
|
| Config write | `~/.config/openstock/.env`에 `KIS_APPKEY`, `KIS_APPSECRET`, `KIS_ACCESS_TOKEN`, `KIS_ACCESS_TOKEN_EXPIRED_AT`, `KIS_TOKEN_TYPE`, `KIS_ACCESS_TOKEN_EXPIRES_IN` |
|
||||||
| External IO | `POST https://openapi.koreainvestment.com:9443/oauth2/tokenP` only when token is missing/expired or `--force` is used |
|
| External IO | `POST https://openapi.koreainvestment.com:9443/oauth2/tokenP` only when token is missing/expired or `--force` is used |
|
||||||
| Output fields | `broker`, `status`, `force`, `credential_source`, `token_storage`, `side_effect` |
|
| Output fields | `broker`, `status`, `force`, `credential_source`, `token_storage`, `side_effect` |
|
||||||
| Side effect | writes auth state to `.env` |
|
| Side effect | writes auth state to `~/.config/openstock/.env` |
|
||||||
|
|
||||||
### `openstock api call <endpoint> --param KEY=VALUE`
|
### `openstock api call <endpoint> --param KEY=VALUE`
|
||||||
|
|
||||||
@@ -480,7 +496,7 @@ KIS endpoint를 직접 GET 호출합니다. `tr_id` 파라미터는 HTTP header
|
|||||||
| Input | `endpoint`, repeated `--param KEY=VALUE` |
|
| Input | `endpoint`, repeated `--param KEY=VALUE` |
|
||||||
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET` |
|
| Env read | `KIS_ACCESS_TOKEN`, `KIS_APPKEY`, `KIS_APPSECRET` |
|
||||||
| External IO | KIS endpoint GET |
|
| External IO | KIS endpoint GET |
|
||||||
| File IO | `.env` read |
|
| File IO | `~/.config/openstock/.env` read |
|
||||||
| Output fields | `broker`, `endpoint`, `params`, `request_semantics`, `response`, `response_semantics` |
|
| Output fields | `broker`, `endpoint`, `params`, `request_semantics`, `response`, `response_semantics` |
|
||||||
| Raw | parsed KIS response |
|
| Raw | parsed KIS response |
|
||||||
| Side effect | endpoint에 따라 다름. typed command 사용 권장 |
|
| Side effect | endpoint에 따라 다름. typed command 사용 권장 |
|
||||||
@@ -508,7 +524,7 @@ KIND 상장법인목록을 받아 stock universe cache를 갱신합니다.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Input | `--force` |
|
| Input | `--force` |
|
||||||
| External IO | `GET https://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13` |
|
| External IO | `GET https://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13` |
|
||||||
| File write | `.openstock/universe/kind/latest.json`, `meta.json`, `YYYY-MM-DD.json` |
|
| File write | `~/.config/openstock/cache/universe/kind/latest.json`, `meta.json`, `YYYY-MM-DD.json` |
|
||||||
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `stock_count`, `counts_by_market` |
|
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `stock_count`, `counts_by_market` |
|
||||||
| Raw | universe metadata |
|
| Raw | universe metadata |
|
||||||
| Side effect | writes/updates local cache; prunes old snapshots |
|
| Side effect | writes/updates local cache; prunes old snapshots |
|
||||||
@@ -518,7 +534,7 @@ KIND 상장법인목록을 받아 stock universe cache를 갱신합니다.
|
|||||||
| Direction | Data |
|
| Direction | Data |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Input | 없음 |
|
| Input | 없음 |
|
||||||
| File read | `.openstock/universe/kind/latest.json`, `meta.json` |
|
| File read | `~/.config/openstock/cache/universe/kind/latest.json`, `meta.json` |
|
||||||
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `stock_count`, `counts_by_market` |
|
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `stock_count`, `counts_by_market` |
|
||||||
| Side effect | 없음 |
|
| Side effect | 없음 |
|
||||||
|
|
||||||
@@ -592,7 +608,7 @@ OpenDART 고유번호 ZIP/XML을 받아 상장사 `stock_code` to `corp_code` ca
|
|||||||
| Input | `--force` |
|
| Input | `--force` |
|
||||||
| Env read | `OPENDART_API_KEY` |
|
| Env read | `OPENDART_API_KEY` |
|
||||||
| External IO | `GET https://opendart.fss.or.kr/api/corpCode.xml` |
|
| External IO | `GET https://opendart.fss.or.kr/api/corpCode.xml` |
|
||||||
| File write | `.openstock/opendart/corp_codes.json`, `corp_codes_meta.json` |
|
| File write | `~/.config/openstock/cache/opendart/corp_codes.json`, `corp_codes_meta.json` |
|
||||||
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `total_count`, `listed_count` |
|
| Output fields | `source`, `cache_date`, `refreshed_at`, `refreshed`, `total_count`, `listed_count` |
|
||||||
| Side effect | writes/updates local cache |
|
| Side effect | writes/updates local cache |
|
||||||
|
|
||||||
@@ -642,7 +658,7 @@ KRX 종목코드를 OpenDART 고유번호로 변환합니다.
|
|||||||
| Input | `rcept_no`: 14자리 접수번호, `--force`, `--max-chars` |
|
| Input | `rcept_no`: 14자리 접수번호, `--force`, `--max-chars` |
|
||||||
| Env read | `OPENDART_API_KEY` |
|
| Env read | `OPENDART_API_KEY` |
|
||||||
| External IO | `GET https://opendart.fss.or.kr/api/document.xml` |
|
| External IO | `GET https://opendart.fss.or.kr/api/document.xml` |
|
||||||
| File read/write | `.openstock/opendart/documents/{rcept_no}.zip` |
|
| File read/write | `~/.config/openstock/cache/opendart/documents/{rcept_no}.zip` |
|
||||||
| Output fields | `rcept_no`, `source`, `viewer_url`, `cached`, `zip_path`, `zip_bytes`, `files`, `text`, `text_chars`, `truncated` |
|
| Output fields | `rcept_no`, `source`, `viewer_url`, `cached`, `zip_path`, `zip_bytes`, `files`, `text`, `text_chars`, `truncated` |
|
||||||
| Raw | same document object |
|
| Raw | same document object |
|
||||||
| Side effect | writes ZIP cache; prunes old document ZIP files |
|
| Side effect | writes ZIP cache; prunes old document ZIP files |
|
||||||
@@ -720,7 +736,7 @@ KIS 계좌 잔고와 보유종목을 조회합니다.
|
|||||||
| Direction | Data |
|
| Direction | Data |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Input | 없음 |
|
| Input | 없음 |
|
||||||
| File read | `.openstock` recursively |
|
| File read | `~/.config/openstock/cache` recursively |
|
||||||
| Output fields | `root`, `exists`, `total_files`, `total_bytes`, `namespaces` |
|
| Output fields | `root`, `exists`, `total_files`, `total_bytes`, `namespaces` |
|
||||||
| Side effect | 없음 |
|
| Side effect | 없음 |
|
||||||
|
|
||||||
@@ -731,7 +747,7 @@ KIS 계좌 잔고와 보유종목을 조회합니다.
|
|||||||
| Direction | Data |
|
| Direction | Data |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Input | `--dry-run` |
|
| Input | `--dry-run` |
|
||||||
| File read | `.openstock` cache directories |
|
| File read | `~/.config/openstock/cache` directories |
|
||||||
| File delete | old universe snapshots, old OpenDART document ZIPs unless `--dry-run` |
|
| File delete | old universe snapshots, old OpenDART document ZIPs unless `--dry-run` |
|
||||||
| Output fields | `dry_run`, `deleted_files`, `deleted_bytes`, `reports` |
|
| Output fields | `dry_run`, `deleted_files`, `deleted_bytes`, `reports` |
|
||||||
| Side effect | deletes local cache files unless dry-run |
|
| Side effect | deletes local cache files unless dry-run |
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::core::dotenv;
|
use crate::core::dotenv;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub(crate) struct AccountConfig {
|
pub(crate) struct AccountConfig {
|
||||||
pub(crate) number: String,
|
pub(crate) number: String,
|
||||||
@@ -13,7 +12,7 @@ impl AccountConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_account_config() -> Result<AccountConfig, String> {
|
pub(crate) fn read_account_config() -> Result<AccountConfig, String> {
|
||||||
let env = dotenv::read_env(Path::new(".env"));
|
let env = dotenv::read_env(&crate::core::paths::env_file());
|
||||||
|
|
||||||
if let Some(account) = env
|
if let Some(account) = env
|
||||||
.get("KIS_ACCOUNT")
|
.get("KIS_ACCOUNT")
|
||||||
@@ -38,7 +37,10 @@ pub(crate) fn read_account_config() -> Result<AccountConfig, String> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("계좌 설정이 없습니다. .env에 KIS_ACCOUNT=12345678-01 또는 KIS_CANO/KIS_ACNT_PRDT_CD를 설정하세요.".to_string())
|
Err(format!(
|
||||||
|
"계좌 설정이 없습니다. {}에 KIS_ACCOUNT=12345678-01 또는 KIS_CANO/KIS_ACNT_PRDT_CD를 설정하세요.",
|
||||||
|
crate::core::paths::env_file().display()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_account(account: &str) -> (&str, &str) {
|
fn split_account(account: &str) -> (&str, &str) {
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
use crate::apis::kis::KisApi;
|
use crate::apis::kis::KisApi;
|
||||||
use crate::core::dotenv;
|
use crate::core::dotenv;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
const KIS_BASE_URL: &str = "https://openapi.koreainvestment.com:9443";
|
const KIS_BASE_URL: &str = "https://openapi.koreainvestment.com:9443";
|
||||||
|
|
||||||
pub fn call(api: &KisApi, endpoint: &str, params: &[(&str, &str)]) -> Result<String, String> {
|
pub fn call(api: &KisApi, endpoint: &str, params: &[(&str, &str)]) -> Result<String, String> {
|
||||||
let env = dotenv::read_env(Path::new(".env"));
|
let env = dotenv::read_env(&crate::core::paths::env_file());
|
||||||
let token = api
|
let token = api
|
||||||
.token()
|
.token()
|
||||||
.or_else(|| env.get("KIS_ACCESS_TOKEN").map(String::as_str))
|
.or_else(|| env.get("KIS_ACCESS_TOKEN").map(String::as_str))
|
||||||
.ok_or("로그인이 필요합니다. `openstock api login`을 먼저 실행하세요.")?;
|
.ok_or("로그인이 필요합니다. `openstock api login`을 먼저 실행하세요.")?;
|
||||||
let appkey = env
|
let appkey = env
|
||||||
.get("KIS_APPKEY")
|
.get("KIS_APPKEY")
|
||||||
.ok_or("KIS_APPKEY가 .env에 없습니다.")?;
|
.ok_or("KIS_APPKEY가 설정 파일에 없습니다.")?;
|
||||||
let appsecret = env
|
let appsecret = env
|
||||||
.get("KIS_APPSECRET")
|
.get("KIS_APPSECRET")
|
||||||
.ok_or("KIS_APPSECRET가 .env에 없습니다.")?;
|
.ok_or("KIS_APPSECRET가 설정 파일에 없습니다.")?;
|
||||||
let tr_id = params
|
let tr_id = params
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(key, _)| key.eq_ignore_ascii_case("tr_id"))
|
.find(|(key, _)| key.eq_ignore_ascii_case("tr_id"))
|
||||||
|
|||||||
+9
-10
@@ -1,6 +1,5 @@
|
|||||||
use crate::core::{dotenv, LoginArguments};
|
use crate::core::{dotenv, LoginArguments};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
const KIS_BASE_URL: &str = "https://openapi.koreainvestment.com:9443";
|
const KIS_BASE_URL: &str = "https://openapi.koreainvestment.com:9443";
|
||||||
@@ -34,10 +33,10 @@ impl LoginArguments for KisLoginArguments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// KIS 실전 REST 접근토큰을 발급하고 .env에 저장한다.
|
/// KIS 실전 REST 접근토큰을 발급하고 설정 파일에 저장한다.
|
||||||
pub fn login(args: &KisLoginArguments) -> Result<String, String> {
|
pub fn login(args: &KisLoginArguments) -> Result<String, String> {
|
||||||
let env_path = Path::new(".env");
|
let env_path = crate::core::paths::env_file();
|
||||||
let env = dotenv::read_env(env_path);
|
let env = dotenv::read_env(&env_path);
|
||||||
if !args.force {
|
if !args.force {
|
||||||
if let Some(access_token) = valid_access_token(&env) {
|
if let Some(access_token) = valid_access_token(&env) {
|
||||||
return Ok(access_token.to_string());
|
return Ok(access_token.to_string());
|
||||||
@@ -99,16 +98,16 @@ pub fn login(args: &KisLoginArguments) -> Result<String, String> {
|
|||||||
.and_then(|value| value.as_u64())
|
.and_then(|value| value.as_u64())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
dotenv::write_env(env_path, "KIS_APPKEY", &args.appkey)?;
|
dotenv::write_env(&env_path, "KIS_APPKEY", &args.appkey)?;
|
||||||
dotenv::write_env(env_path, "KIS_APPSECRET", &args.appsecret)?;
|
dotenv::write_env(&env_path, "KIS_APPSECRET", &args.appsecret)?;
|
||||||
dotenv::write_env(env_path, args.token_env_key(), &access_token)?;
|
dotenv::write_env(&env_path, args.token_env_key(), &access_token)?;
|
||||||
dotenv::write_env(env_path, "KIS_TOKEN_TYPE", token_type)?;
|
dotenv::write_env(&env_path, "KIS_TOKEN_TYPE", token_type)?;
|
||||||
if !expires_at.is_empty() {
|
if !expires_at.is_empty() {
|
||||||
dotenv::write_env(env_path, KIS_ACCESS_TOKEN_EXPIRED_AT_KEY, expires_at)?;
|
dotenv::write_env(&env_path, KIS_ACCESS_TOKEN_EXPIRED_AT_KEY, expires_at)?;
|
||||||
}
|
}
|
||||||
if expires_in > 0 {
|
if expires_in > 0 {
|
||||||
dotenv::write_env(
|
dotenv::write_env(
|
||||||
env_path,
|
&env_path,
|
||||||
"KIS_ACCESS_TOKEN_EXPIRES_IN",
|
"KIS_ACCESS_TOKEN_EXPIRES_IN",
|
||||||
&expires_in.to_string(),
|
&expires_in.to_string(),
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::apis::kis::KisApi;
|
|||||||
use crate::core::dotenv;
|
use crate::core::dotenv;
|
||||||
use crate::core::TraderApi;
|
use crate::core::TraderApi;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
const KIS_BASE_URL: &str = "https://openapi.koreainvestment.com:9443";
|
const KIS_BASE_URL: &str = "https://openapi.koreainvestment.com:9443";
|
||||||
const ORDER_CASH_ENDPOINT: &str = "/uapi/domestic-stock/v1/trading/order-cash";
|
const ORDER_CASH_ENDPOINT: &str = "/uapi/domestic-stock/v1/trading/order-cash";
|
||||||
@@ -72,17 +71,17 @@ pub(crate) fn order_cash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn post_order(api: &KisApi, tr_id: &str, body: &serde_json::Value) -> Result<String, String> {
|
fn post_order(api: &KisApi, tr_id: &str, body: &serde_json::Value) -> Result<String, String> {
|
||||||
let env = dotenv::read_env(Path::new(".env"));
|
let env = dotenv::read_env(&crate::core::paths::env_file());
|
||||||
let token = api
|
let token = api
|
||||||
.token()
|
.token()
|
||||||
.or_else(|| env.get("KIS_ACCESS_TOKEN").map(String::as_str))
|
.or_else(|| env.get("KIS_ACCESS_TOKEN").map(String::as_str))
|
||||||
.ok_or("로그인이 필요합니다. `openstock api login`을 먼저 실행하세요.")?;
|
.ok_or("로그인이 필요합니다. `openstock api login`을 먼저 실행하세요.")?;
|
||||||
let appkey = env
|
let appkey = env
|
||||||
.get("KIS_APPKEY")
|
.get("KIS_APPKEY")
|
||||||
.ok_or("KIS_APPKEY가 .env에 없습니다.")?;
|
.ok_or("KIS_APPKEY가 설정 파일에 없습니다.")?;
|
||||||
let appsecret = env
|
let appsecret = env
|
||||||
.get("KIS_APPSECRET")
|
.get("KIS_APPSECRET")
|
||||||
.ok_or("KIS_APPSECRET가 .env에 없습니다.")?;
|
.ok_or("KIS_APPSECRET가 설정 파일에 없습니다.")?;
|
||||||
let url = format!("{}{}", KIS_BASE_URL, ORDER_CASH_ENDPOINT);
|
let url = format!("{}{}", KIS_BASE_URL, ORDER_CASH_ENDPOINT);
|
||||||
|
|
||||||
let response = crate::core::http::agent()
|
let response = crate::core::http::agent()
|
||||||
|
|||||||
+14
-14
@@ -2,7 +2,6 @@ use crate::apis::create_default_registry;
|
|||||||
use crate::apis::kis::{login::KisLoginArguments, KisApi};
|
use crate::apis::kis::{login::KisLoginArguments, KisApi};
|
||||||
use crate::core::TraderApi;
|
use crate::core::TraderApi;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum ApiCommands {
|
pub enum ApiCommands {
|
||||||
@@ -18,11 +17,11 @@ pub enum ApiCommands {
|
|||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct KisLoginCommand {
|
pub struct KisLoginCommand {
|
||||||
/// 한국투자증권 Open API 앱키 (.env의 KIS_APPKEY보다 우선)
|
/// 한국투자증권 Open API 앱키 (설정 파일의 KIS_APPKEY보다 우선)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
appkey: Option<String>,
|
appkey: Option<String>,
|
||||||
|
|
||||||
/// 한국투자증권 Open API 앱시크릿 (.env의 KIS_APPSECRET보다 우선)
|
/// 한국투자증권 Open API 앱시크릿 (설정 파일의 KIS_APPSECRET보다 우선)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
appsecret: Option<String>,
|
appsecret: Option<String>,
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ pub fn handle_api(sub: &ApiCommands) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
ApiCommands::Login(command) => {
|
ApiCommands::Login(command) => {
|
||||||
let env = crate::core::dotenv::read_env(Path::new(".env"));
|
let env = crate::core::dotenv::read_env(&crate::core::paths::env_file());
|
||||||
|
|
||||||
let appkey = command
|
let appkey = command
|
||||||
.appkey
|
.appkey
|
||||||
@@ -111,10 +110,10 @@ pub fn handle_api(sub: &ApiCommands) {
|
|||||||
),
|
),
|
||||||
crate::core::output::field(
|
crate::core::output::field(
|
||||||
"credential_source",
|
"credential_source",
|
||||||
"appkey/appsecret 입력 출처. CLI 옵션이 있으면 우선 사용하고 없으면 .env의 KIS_APPKEY/KIS_APPSECRET을 사용한다.",
|
"appkey/appsecret 입력 출처. CLI 옵션이 있으면 우선 사용하고 없으면 설정 파일의 KIS_APPKEY/KIS_APPSECRET을 사용한다.",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"appkey": if command.appkey.is_some() { "cli_argument" } else { ".env:KIS_APPKEY" },
|
"appkey": if command.appkey.is_some() { "cli_argument" } else { "config_file:KIS_APPKEY" },
|
||||||
"appsecret": if command.appsecret.is_some() { "cli_argument" } else { ".env:KIS_APPSECRET" },
|
"appsecret": if command.appsecret.is_some() { "cli_argument" } else { "config_file:KIS_APPSECRET" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
crate::core::output::field(
|
crate::core::output::field(
|
||||||
@@ -122,13 +121,14 @@ pub fn handle_api(sub: &ApiCommands) {
|
|||||||
"발급 또는 재사용된 접근토큰이 저장되는 위치와 키",
|
"발급 또는 재사용된 접근토큰이 저장되는 위치와 키",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"file": ".env",
|
"file": ".env",
|
||||||
|
"path": crate::core::paths::env_file().display().to_string(),
|
||||||
"access_token_key": "KIS_ACCESS_TOKEN",
|
"access_token_key": "KIS_ACCESS_TOKEN",
|
||||||
"expiration_key": "KIS_ACCESS_TOKEN_EXPIRED_AT",
|
"expiration_key": "KIS_ACCESS_TOKEN_EXPIRED_AT",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
crate::core::output::field(
|
crate::core::output::field(
|
||||||
"side_effect",
|
"side_effect",
|
||||||
"명령의 외부 부작용. 인증 토큰과 인증 정보를 .env에 저장할 수 있지만 금융 주문은 발생하지 않는다.",
|
"명령의 외부 부작용. 인증 토큰과 인증 정보를 설정 파일에 저장할 수 있지만 금융 주문은 발생하지 않는다.",
|
||||||
serde_json::json!("writes_auth_state"),
|
serde_json::json!("writes_auth_state"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -234,25 +234,25 @@ fn api_catalog(api: &dyn TraderApi) -> serde_json::Value {
|
|||||||
"name": "KIS_APPKEY",
|
"name": "KIS_APPKEY",
|
||||||
"description": "KIS Open API application key used for authentication and every broker API request.",
|
"description": "KIS Open API application key used for authentication and every broker API request.",
|
||||||
"required_for": ["api login", "api call", "account status", "market", "market history", "order buy", "order sell", "order status"],
|
"required_for": ["api login", "api call", "account status", "market", "market history", "order buy", "order sell", "order status"],
|
||||||
"source": ".env or api login --appkey"
|
"source": "~/.config/openstock/.env or api login --appkey"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KIS_APPSECRET",
|
"name": "KIS_APPSECRET",
|
||||||
"description": "KIS Open API application secret used for authentication and every broker API request.",
|
"description": "KIS Open API application secret used for authentication and every broker API request.",
|
||||||
"required_for": ["api login", "api call", "account status", "market", "market history", "order buy", "order sell", "order status"],
|
"required_for": ["api login", "api call", "account status", "market", "market history", "order buy", "order sell", "order status"],
|
||||||
"source": ".env or api login --appsecret"
|
"source": "~/.config/openstock/.env or api login --appsecret"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KIS_ACCESS_TOKEN",
|
"name": "KIS_ACCESS_TOKEN",
|
||||||
"description": "Bearer access token issued by api login. Read commands and order commands require it.",
|
"description": "Bearer access token issued by api login. Read commands and order commands require it.",
|
||||||
"required_for": ["api call", "account status", "market", "market history", "order buy", "order sell", "order status"],
|
"required_for": ["api call", "account status", "market", "market history", "order buy", "order sell", "order status"],
|
||||||
"source": ".env written by api login"
|
"source": "~/.config/openstock/.env written by api login"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "KIS_ACCOUNT",
|
"name": "KIS_ACCOUNT",
|
||||||
"description": "Live account identifier in CANO-ACNT_PRDT_CD format, for example 12345678-01. Required for account and order commands.",
|
"description": "Live account identifier in CANO-ACNT_PRDT_CD format, for example 12345678-01. Required for account and order commands.",
|
||||||
"required_for": ["account status", "order buy", "order sell", "order status"],
|
"required_for": ["account status", "order buy", "order sell", "order status"],
|
||||||
"source": ".env"
|
"source": "~/.config/openstock/.env"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
@@ -260,8 +260,8 @@ fn api_catalog(api: &dyn TraderApi) -> serde_json::Value {
|
|||||||
"command": "api login",
|
"command": "api login",
|
||||||
"purpose": "Prepare authentication by issuing or reusing a live KIS access token.",
|
"purpose": "Prepare authentication by issuing or reusing a live KIS access token.",
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{"name": "--appkey", "required": false, "description": "Overrides .env KIS_APPKEY."},
|
{"name": "--appkey", "required": false, "description": "Overrides configured KIS_APPKEY."},
|
||||||
{"name": "--appsecret", "required": false, "description": "Overrides .env KIS_APPSECRET."},
|
{"name": "--appsecret", "required": false, "description": "Overrides configured KIS_APPSECRET."},
|
||||||
{"name": "--force", "required": false, "description": "Issue a new token even if a valid token already exists."}
|
{"name": "--force", "required": false, "description": "Issue a new token even if a valid token already exists."}
|
||||||
],
|
],
|
||||||
"output_contract": "Explained JSON with broker, status, force, credential_source, token_storage, and side_effect fields.",
|
"output_contract": "Explained JSON with broker, status, force, credential_source, token_storage, and side_effect fields.",
|
||||||
|
|||||||
+4
-5
@@ -4,8 +4,6 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
const CACHE_ROOT: &str = ".openstock";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct CacheStatus {
|
pub struct CacheStatus {
|
||||||
pub root: String,
|
pub root: String,
|
||||||
@@ -51,10 +49,11 @@ struct SnapshotFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn status() -> Result<CacheStatus, String> {
|
pub fn status() -> Result<CacheStatus, String> {
|
||||||
let root = Path::new(CACHE_ROOT);
|
let root = crate::core::paths::cache_dir();
|
||||||
|
let root = root.as_path();
|
||||||
if !root.exists() {
|
if !root.exists() {
|
||||||
return Ok(CacheStatus {
|
return Ok(CacheStatus {
|
||||||
root: CACHE_ROOT.to_string(),
|
root: root.display().to_string(),
|
||||||
exists: false,
|
exists: false,
|
||||||
total_files: 0,
|
total_files: 0,
|
||||||
total_bytes: 0,
|
total_bytes: 0,
|
||||||
@@ -74,7 +73,7 @@ pub fn status() -> Result<CacheStatus, String> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(CacheStatus {
|
Ok(CacheStatus {
|
||||||
root: CACHE_ROOT.to_string(),
|
root: root.display().to_string(),
|
||||||
exists: true,
|
exists: true,
|
||||||
total_files,
|
total_files,
|
||||||
total_bytes,
|
total_bytes,
|
||||||
|
|||||||
@@ -32,5 +32,8 @@ pub fn write_env(path: &Path, key: &str, value: &str) -> Result<(), String> {
|
|||||||
for (k, v) in &map {
|
for (k, v) in &map {
|
||||||
content.push_str(&format!("{}={}\n", k, v));
|
content.push_str(&format!("{}={}\n", k, v));
|
||||||
}
|
}
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| format!(".env 디렉터리 생성 실패: {}", e))?;
|
||||||
|
}
|
||||||
fs::write(path, content).map_err(|e| format!(".env 쓰기 실패: {}", e))
|
fs::write(path, content).map_err(|e| format!(".env 쓰기 실패: {}", e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod cache;
|
|||||||
pub mod dotenv;
|
pub mod dotenv;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
pub mod paths;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod stock;
|
pub mod stock;
|
||||||
pub mod trader;
|
pub mod trader;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
const APP_DIR: &str = "openstock";
|
||||||
|
|
||||||
|
pub fn config_dir() -> PathBuf {
|
||||||
|
if let Ok(value) = env::var("OPENSTOCK_CONFIG_DIR") {
|
||||||
|
let value = value.trim();
|
||||||
|
if !value.is_empty() {
|
||||||
|
return PathBuf::from(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
home_dir()
|
||||||
|
.map(|home| home.join(".config").join(APP_DIR))
|
||||||
|
.unwrap_or_else(|| PathBuf::from(".").join(".config").join(APP_DIR))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn env_file() -> PathBuf {
|
||||||
|
config_dir().join(".env")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_dir() -> PathBuf {
|
||||||
|
config_dir().join("cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_namespace(path: &str) -> PathBuf {
|
||||||
|
cache_dir().join(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home_dir() -> Option<PathBuf> {
|
||||||
|
env::var_os("HOME")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn env_file_lives_under_config_dir() {
|
||||||
|
let path = env_file();
|
||||||
|
assert!(path.ends_with(".env"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_dir_lives_under_config_dir() {
|
||||||
|
let path = cache_namespace("universe/kind");
|
||||||
|
assert!(path.ends_with("cache/universe/kind"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -17,7 +17,7 @@ impl TraderBase {
|
|||||||
|
|
||||||
/// 증권사별 로그인에 필요한 인자들의 공통 트레이트
|
/// 증권사별 로그인에 필요한 인자들의 공통 트레이트
|
||||||
pub trait LoginArguments {
|
pub trait LoginArguments {
|
||||||
/// .env 파일에 저장할 토큰 키 이름 (예: "KIS_ACCESS_TOKEN")
|
/// 설정 파일에 저장할 토큰 키 이름 (예: "KIS_ACCESS_TOKEN")
|
||||||
fn token_env_key(&self) -> &'static str;
|
fn token_env_key(&self) -> &'static str;
|
||||||
|
|
||||||
/// 구체적인 타입으로 다운캐스팅하기 위한 Any 변환
|
/// 구체적인 타입으로 다운캐스팅하기 위한 Any 변환
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
const KIND_UNIVERSE_URL: &str =
|
const KIND_UNIVERSE_URL: &str =
|
||||||
"https://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13";
|
"https://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13";
|
||||||
const CACHE_DIR: &str = ".openstock/universe/kind";
|
|
||||||
const LATEST_FILE: &str = "latest.json";
|
const LATEST_FILE: &str = "latest.json";
|
||||||
const META_FILE: &str = "meta.json";
|
const META_FILE: &str = "meta.json";
|
||||||
const SNAPSHOT_RETENTION_FILES: usize = 7;
|
const SNAPSHOT_RETENTION_FILES: usize = 7;
|
||||||
@@ -365,7 +364,7 @@ fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cache_dir() -> PathBuf {
|
fn cache_dir() -> PathBuf {
|
||||||
PathBuf::from(CACHE_DIR)
|
crate::core::paths::cache_namespace("universe/kind")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn counts_by_market(stocks: &[Stock]) -> BTreeMap<String, usize> {
|
fn counts_by_market(stocks: &[Stock]) -> BTreeMap<String, usize> {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
const OPENDART_CORP_CODE_URL: &str = "https://opendart.fss.or.kr/api/corpCode.xml";
|
const OPENDART_CORP_CODE_URL: &str = "https://opendart.fss.or.kr/api/corpCode.xml";
|
||||||
const CACHE_DIR: &str = ".openstock/opendart";
|
|
||||||
const CORP_CODES_FILE: &str = "corp_codes.json";
|
const CORP_CODES_FILE: &str = "corp_codes.json";
|
||||||
const META_FILE: &str = "corp_codes_meta.json";
|
const META_FILE: &str = "corp_codes_meta.json";
|
||||||
|
|
||||||
@@ -116,11 +115,16 @@ pub fn find_by_stock_code<'a>(corps: &'a [DartCorpCode], symbol: &str) -> Option
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn api_key() -> Result<String, String> {
|
fn api_key() -> Result<String, String> {
|
||||||
dotenv::read_env(Path::new(".env"))
|
dotenv::read_env(&crate::core::paths::env_file())
|
||||||
.get("OPENDART_API_KEY")
|
.get("OPENDART_API_KEY")
|
||||||
.map(|value| value.trim().to_string())
|
.map(|value| value.trim().to_string())
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or("OPENDART_API_KEY가 .env에 없습니다.".to_string())
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"OPENDART_API_KEY가 설정 파일에 없습니다: {}",
|
||||||
|
crate::core::paths::env_file().display()
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_corp_code_zip(bytes: &[u8]) -> Result<Vec<DartCorpCode>, String> {
|
fn parse_corp_code_zip(bytes: &[u8]) -> Result<Vec<DartCorpCode>, String> {
|
||||||
@@ -249,7 +253,7 @@ fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cache_dir() -> PathBuf {
|
fn cache_dir() -> PathBuf {
|
||||||
PathBuf::from(CACHE_DIR)
|
crate::core::paths::cache_namespace("opendart")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_utc_date_string() -> String {
|
fn current_utc_date_string() -> String {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::core::dotenv;
|
use crate::core::dotenv;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
const OPENDART_LIST_URL: &str = "https://opendart.fss.or.kr/api/list.json";
|
const OPENDART_LIST_URL: &str = "https://opendart.fss.or.kr/api/list.json";
|
||||||
|
|
||||||
@@ -54,11 +53,16 @@ pub fn list(query: &DisclosureQuery) -> Result<serde_json::Value, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn api_key() -> Result<String, String> {
|
fn api_key() -> Result<String, String> {
|
||||||
dotenv::read_env(Path::new(".env"))
|
dotenv::read_env(&crate::core::paths::env_file())
|
||||||
.get("OPENDART_API_KEY")
|
.get("OPENDART_API_KEY")
|
||||||
.map(|value| value.trim().to_string())
|
.map(|value| value.trim().to_string())
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or("OPENDART_API_KEY가 .env에 없습니다.".to_string())
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"OPENDART_API_KEY가 설정 파일에 없습니다: {}",
|
||||||
|
crate::core::paths::env_file().display()
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_query(query: &DisclosureQuery) -> Result<(), String> {
|
fn validate_query(query: &DisclosureQuery) -> Result<(), String> {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use std::io::{Cursor, Read};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
const OPENDART_DOCUMENT_URL: &str = "https://opendart.fss.or.kr/api/document.xml";
|
const OPENDART_DOCUMENT_URL: &str = "https://opendart.fss.or.kr/api/document.xml";
|
||||||
const CACHE_DIR: &str = ".openstock/opendart/documents";
|
|
||||||
const DOCUMENT_RETENTION_FILES: usize = 100;
|
const DOCUMENT_RETENTION_FILES: usize = 100;
|
||||||
const DOCUMENT_RETENTION_BYTES: u64 = 200 * 1024 * 1024;
|
const DOCUMENT_RETENTION_BYTES: u64 = 200 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -302,15 +301,20 @@ fn cache_path(rcept_no: &str) -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cache_dir() -> PathBuf {
|
fn cache_dir() -> PathBuf {
|
||||||
PathBuf::from(CACHE_DIR)
|
crate::core::paths::cache_namespace("opendart/documents")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_key() -> Result<String, String> {
|
fn api_key() -> Result<String, String> {
|
||||||
dotenv::read_env(Path::new(".env"))
|
dotenv::read_env(&crate::core::paths::env_file())
|
||||||
.get("OPENDART_API_KEY")
|
.get("OPENDART_API_KEY")
|
||||||
.map(|value| value.trim().to_string())
|
.map(|value| value.trim().to_string())
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or("OPENDART_API_KEY가 .env에 없습니다.".to_string())
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"OPENDART_API_KEY가 설정 파일에 없습니다: {}",
|
||||||
|
crate::core::paths::env_file().display()
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_rcept_no(value: &str) -> Result<(), String> {
|
fn validate_rcept_no(value: &str) -> Result<(), String> {
|
||||||
|
|||||||
Reference in New Issue
Block a user