Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 0 additions & 56 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,59 +44,3 @@ python -m pytest -q
| **Azure** | `QSL_CLOUD_PROVIDER=azure` | 使用 Azure Key Vault、Blob Storage、Cosmos DB—— 需要 azure-identity 和 Azure SDK。 |
| **本地文件系统** | `QSL_CLOUD_PROVIDER=local` | 密钥和数据库存储在 `~/.qsl/` 目录下。无需任何云凭证——适合开发、测试和离线环境。 |
| **环境变量** | `QSL_CLOUD_PROVIDER=env` | 密钥从环境变量读取;其余操作使用本地文件系统。适合 CI 场景。 |

**用法:**

```python
from quant_platform_kit.cloud import (
get_secret_store, # SecretStore(只读)
get_secret_store_rw, # SecretStoreReadWrite(令牌刷新等写场景)
get_object_store, # ObjectStore(GCS / S3 / 本地文件)
get_document_store, # DocumentStore(Firestore / JSON 文件)
get_compute_discovery, # ComputeDiscovery(GCE / 环境变量)
get_deployment_context, # DeploymentContext(Cloud Run / 本地 mock)
)

# 读取密钥——无论后端是 GCP、环境变量还是 ~/.qsl/secrets/ 都可以
secret = get_secret_store().get_secret("my-api-key")

# 读写对象——URI 格式与 provider 无关
data = get_object_store().read_text("gs://bucket/path/to/data.json")
get_object_store().write_text("gs://bucket/path/to/output.json", '{"key": "value"}')
```

切换 Provider 只需设置 `QSL_CLOUD_PROVIDER` 环境变量:

```bash
export QSL_CLOUD_PROVIDER=local # 所有云操作走 ~/.qsl/ 本地目录
python your_script.py
```

令牌刷新场景(如 LongPort 或 Schwab OAuth 自动续期)使用读写版接口:

```python
from quant_platform_kit.cloud import get_secret_store_rw
rw = get_secret_store_rw()
rw.update_secret("my-token", "new-token-value")
```

## 延伸文档

- [`docs/platform_notification_outcomes.md`](docs/platform_notification_outcomes.md)
- [`docs/platform_notification_outcomes.zh-CN.md`](docs/platform_notification_outcomes.zh-CN.md)
- [`docs/platform_repo_boundaries.md`](docs/platform_repo_boundaries.md)
- [`docs/platform_repo_boundaries.zh-CN.md`](docs/platform_repo_boundaries.zh-CN.md)
- [`docs/quantconnect.md`](docs/quantconnect.md)
- [`docs/strategy_plugin_runtime_contract.md`](docs/strategy_plugin_runtime_contract.md)
- [`docs/strategy_plugin_runtime_contract.zh-CN.md`](docs/strategy_plugin_runtime_contract.zh-CN.md)
- [`docs/us_equity_cross_platform_strategy_spec.md`](docs/us_equity_cross_platform_strategy_spec.md)

## 社区和安全

- 贡献前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md),确认 PR 范围、本地校验和文档要求。
- 讨论、issue 和 review 请遵守 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。
- 涉及密钥、自动化、券商/交易所或云资源的漏洞请按 [SECURITY.md](SECURITY.md) 私密报告;不要为 secret 或实盘风险开公开 issue。

## 许可证

详见 [LICENSE](LICENSE)。
104 changes: 98 additions & 6 deletions src/quant_platform_kit/cloud/aws_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,21 +242,65 @@ def resolve_instance_ip(


class AwsDeploymentContext:
"""AWS deployment context."""
"""AWS deployment context.

通过 ECS metadata / EC2 IMDSv2 获取运行时身份信息。

注意:fetch_id_token 的语义与 GCP 不完全对等 ——
AWS 没有 GCP ID Token 的标准替代品。
此实现返回当前 EC2/ECS 实例的身份文档,
或 STS GetCallerIdentity 作为 fallback。
"""

@property
def project_id(self) -> str:
return os.environ.get("AWS_ACCOUNT_ID", "")
# ECS task ARN 解析 → account_id
task_arn = os.environ.get("ECS_CONTAINER_METADATA_URI_V4", "")
if task_arn:
parts = task_arn.split(":") if ":" in task_arn else []
if len(parts) >= 5:
return parts[4]
return _resolve_aws_account_id()

@property
def region(self) -> str | None:
return _resolve_aws_region()

def fetch_id_token(self, audience: str) -> str:
raise NotImplementedError(
"AWS DeploymentContext.fetch_id_token is not implemented. "
"Use a service-specific mechanism (e.g., Cognito, STS, or IAM roles)."
)
"""获取当前 AWS 环境的身份凭证。

在 ECS/EC2 上返回实例身份文档的 JSON string;
本地开发环境 fallback 到 STS GetCallerIdentity。

``audience`` 参数在 AWS 无直接对应,此方法不会使用它。
如果调用方依赖 audience 进行令牌验证,
建议使用 Cognito 或自建 OIDC provider。
"""
import json as _json
try:
return _fetch_ecs_identity()
except Exception:
pass
try:
return _fetch_ec2_identity()
except Exception:
pass
try:
import boto3
sts = boto3.client("sts", region_name=_resolve_aws_region())
identity = sts.get_caller_identity()
return _json.dumps({
"account": identity.get("Account", ""),
"arn": identity.get("Arn", ""),
"user_id": identity.get("UserId", ""),
})
except Exception as exc:
raise RuntimeError(
"AwsDeploymentContext.fetch_id_token: unable to resolve AWS identity. "
"Ensure the process runs on EC2, ECS, or has valid AWS credentials. "
"Note: GCP-style audience-based ID tokens are not natively supported on AWS; "
"consider using Cognito or a custom OIDC setup for audienced tokens."
) from exc


# ══════════════════════════════════════════════════════════════════════
Expand All @@ -279,6 +323,54 @@ def _resolve_aws_region() -> str:
return "us-east-1"


def _resolve_aws_account_id() -> str:
"""Resolve AWS account ID from env vars, STS, or metadata."""
env = os.environ.get("AWS_ACCOUNT_ID")
if env:
return env
try:
import boto3
sts = boto3.client("sts", region_name=_resolve_aws_region())
return sts.get_caller_identity().get("Account", "")
except Exception:
return ""


def _fetch_ecs_identity() -> str:
"""Fetch identity document from ECS container metadata endpoint (v4)."""
import urllib.request
metadata_uri = os.environ.get("ECS_CONTAINER_METADATA_URI_V4", "")
if not metadata_uri:
raise RuntimeError("Not running on ECS (ECS_CONTAINER_METADATA_URI_V4 not set)")
req = urllib.request.Request(metadata_uri)
with urllib.request.urlopen(req, timeout=3) as resp:
return resp.read().decode("utf-8")


def _fetch_ec2_identity() -> str:
"""Fetch identity document from EC2 IMDSv2."""
import urllib.request
# Step 1: get IMDSv2 token
token_req = urllib.request.Request(
"http://169.254.169.254/latest/api/token",
headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"},
method="PUT",
)
try:
with urllib.request.urlopen(token_req, timeout=2) as resp:
token = resp.read().decode("utf-8")
except Exception:
raise RuntimeError("Not running on EC2 (IMDSv2 unreachable)")

# Step 2: fetch identity document
id_req = urllib.request.Request(
"http://169.254.169.254/latest/dynamic/instance-identity/document",
headers={"X-aws-ec2-metadata-token": token},
)
with urllib.request.urlopen(id_req, timeout=3) as resp:
return resp.read().decode("utf-8")


def _dynamodb_serialize(value):
"""Convert Python values to DynamoDB-compatible format."""
if isinstance(value, (str, bool, int, float, type(None))):
Expand Down
19 changes: 14 additions & 5 deletions src/quant_platform_kit/cloud/env_provider.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
"""
Environment Variable provider — 从 os.environ 读取配置。
适合简单部署场景或 CI 环境,无需云服务 SDK。

适合简单部署场景或 CI 环境,无需任何云服务 SDK。

用法:
QSL_CLOUD_PROVIDER=env
密钥通过环境变量注入(大写+下划线格式)
对象存储通过本地文件系统(类似 local_provider)
对象存储通过本地文件系统(与 local_provider 共享实现)

设计说明:
- SecretStore: 从环境变量读取(优先 QSL_SECRET_<NAME>,其次 <NAME>)
- SecretStoreReadWrite: 只读,写操作 noop(env vars 不可运行时写入)
- ObjectStore / DocumentStore / ComputeDiscovery / DeploymentContext:
直接复用 local_provider 实现。原因:env provider 的定位是"零依赖",
不引入任何云 SDK。对象存储和文档数据库不适合通过环境变量操作,
local 文件系统是开发者/CI 场景下最合理的后端。
"""

from __future__ import annotations
Expand All @@ -21,7 +30,8 @@
LocalDeploymentContext,
)

# Re-use local implementations for object store / doc store / compute
# Re-use local implementations — by design, not a gap.
# In CI and local dev, a real cloud bucket is unnecessary; a tmp dir suffices.
ObjectStore = LocalObjectStore
DocumentStore = LocalDocumentStore
ComputeDiscovery = LocalComputeDiscovery
Expand Down Expand Up @@ -54,13 +64,12 @@ def get_secret(self, secret_name: str, *, project_id: str | None = None) -> str:


class EnvSecretStoreReadWrite:
"""只读 + 占位写操作(env var 不支持写入返回 noop)。"""
"""只读 + 占位写操作(env var 不支持运行时写入)。"""

def get_secret(self, secret_name: str, *, project_id: str | None = None) -> str:
return EnvSecretStore().get_secret(secret_name, project_id=project_id)

def create_secret(self, secret_name: str, payload: str, *, project_id: str | None = None) -> str:
# env vars 不可写,仅记录日志
import logging
logging.getLogger(__name__).warning(
"EnvSecretStore: create_secret is a no-op (env vars cannot be written at runtime). "
Expand Down
35 changes: 26 additions & 9 deletions src/quant_platform_kit/cloud/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

每个 Protocol 定义一个云服务品类(密钥管理、对象存储、文档数据库等),
GcpProvider 是默认实现(保持现有行为不变),
社区可通过 env PROVIDER=aws|local 切换到其他实现。
社区可通过 env PROVIDER=aws|azure|local|env 切换到其他实现。
"""

from __future__ import annotations
Expand Down Expand Up @@ -42,16 +42,33 @@ def destroy_latest_secret(self, secret_name: str, *, project_id: str | None = No


# ──────────────────────────────────────────────────────────────────────
# Object Store — 对象存储(GCS / S3 / 本地文件系统)
# Object Store — 对象存储(GCS / S3 / Azure Blob / 本地文件系统)
# ──────────────────────────────────────────────────────────────────────

@runtime_checkable
class ObjectStore(Protocol):
"""对象存储接口。URI 格式由实现方决定:
- GCP: gs://bucket/key
- AWS: s3://bucket/key
- Azure: az://account/container/blob
- Local: file:///absolute/path 或 /absolute/path

GCP provider:
gs://bucket-name/path/to/key — Cloud Storage

AWS provider:
s3://bucket-name/path/to/key — S3

Azure provider:
az://account/container/blob — Blob Storage

Local / Env provider:
/absolute/path/to/file — 本地文件(绝对路径)
file:///absolute/path/to/file — 同上
gs://bucket/key — 映射到 ~/.qsl/storage/gs/bucket/key
s3://bucket/key — 映射到 ~/.qsl/storage/s3/bucket/key
az://account/container/blob — 映射到 ~/.qsl/storage/az/account/container/blob

注意:URI scheme 与 provider 必须匹配:
- ``s3://`` URI + AWS provider → S3 操作
- ``s3://`` URI + Local provider → 本地文件模拟(便于开发)
- ``gs://`` URI + AWS provider → ValueError(不跨云互通)
"""

def read_text(self, uri: str) -> str:
Expand Down Expand Up @@ -80,7 +97,7 @@ def list(self, prefix: str) -> list[str]:


# ──────────────────────────────────────────────────────────────────────
# Document Store — 文档型 KV(Firestore / DynamoDB)
# Document Store — 文档型 KV(Firestore / DynamoDB / Cosmos DB
# ──────────────────────────────────────────────────────────────────────

@runtime_checkable
Expand All @@ -105,7 +122,7 @@ def delete(self, collection: str, document_id: str) -> None:


# ──────────────────────────────────────────────────────────────────────
# Compute Discovery — 计算资源发现(GCE / EC2)
# Compute Discovery — 计算资源发现(GCE / EC2 / Azure VM
# ──────────────────────────────────────────────────────────────────────

@runtime_checkable
Expand All @@ -125,7 +142,7 @@ def resolve_instance_ip(


# ──────────────────────────────────────────────────────────────────────
# Deployment Context — 部署上下文(Cloud Run / ECS / 自托管)
# Deployment Context — 部署上下文(Cloud Run / ECS / Azure CA / 自托管)
# ──────────────────────────────────────────────────────────────────────

@runtime_checkable
Expand Down
Loading