fufu-cloud-cli 0.9.4__tar.gz → 0.9.6__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.
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/PKG-INFO +49 -12
- fufu_cloud_cli-0.9.4/src/fufu_cloud_cli.egg-info/PKG-INFO → fufu_cloud_cli-0.9.6/README.md +43 -29
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/pyproject.toml +5 -3
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/__init__.py +1 -1
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/__main__.py +1 -1
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/api_backend.py +8 -0
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/aws_errors.py +6 -0
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/aws_output.py +65 -0
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/bigquery.py +92 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/capabilities.json +181 -24
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/cli.py +86 -14
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/control.py +37 -12
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/gcp_infra.py +222 -0
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/gcp_sdk.py +60 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/remote_compute.py +6 -2
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/runtime_sdk.py +44 -12
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/vendor/AWS-CLI-LICENSE.txt +12 -0
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/vendor/__init__.py +1 -0
- fufu_cloud_cli-0.9.6/src/fufu_cloud_cli/vendor/aws_text.py +117 -0
- fufu_cloud_cli-0.9.4/README.md → fufu_cloud_cli-0.9.6/src/fufu_cloud_cli.egg-info/PKG-INFO +66 -11
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli.egg-info/SOURCES.txt +9 -1
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli.egg-info/entry_points.txt +1 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli.egg-info/requires.txt +6 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/LICENSE +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/MANIFEST.in +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/setup.cfg +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/capacity.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/cloud_data.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/cloudflare.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/docker_backend.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/errors.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/gcloud_environment.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/gcloud_output.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/gcs.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/http_runtime.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/iac_state.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/limits.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/manifest.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/object_transfer.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/public_access.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/pubsub.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/pulumi-endpoints.json +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/pulumi_bridge.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/runtime/ingress/proxy.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/secrets.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/serverless.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/__init__.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/aws.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/aws_data.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/common.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/google.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/remote_state.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli/terraform_bridge/runner.py +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
- {fufu_cloud_cli-0.9.4 → fufu_cloud_cli-0.9.6}/src/fufu_cloud_cli.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fufu-cloud-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: FUFU Cloud CLI for tenant-scoped mock cloud development and deployment
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: PyYAML==6.0.3
|
|
10
|
+
Requires-Dist: jmespath==1.1.0
|
|
10
11
|
Requires-Dist: tomli==2.2.1; python_version < "3.11"
|
|
11
12
|
Provides-Extra: yaml
|
|
12
13
|
Requires-Dist: PyYAML==6.0.3; extra == "yaml"
|
|
@@ -14,14 +15,48 @@ Provides-Extra: test
|
|
|
14
15
|
Requires-Dist: boto3==1.40.66; extra == "test"
|
|
15
16
|
Provides-Extra: terraform
|
|
16
17
|
Requires-Dist: python-hcl2==7.3.1; extra == "terraform"
|
|
18
|
+
Provides-Extra: gcp
|
|
19
|
+
Requires-Dist: google-cloud-bigquery==3.38.0; extra == "gcp"
|
|
20
|
+
Requires-Dist: google-cloud-tasks==2.20.0; extra == "gcp"
|
|
21
|
+
Requires-Dist: google-cloud-logging==3.12.1; extra == "gcp"
|
|
17
22
|
Dynamic: license-file
|
|
18
23
|
|
|
19
24
|
# FUFU Cloud CLI
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
26
|
+
以六個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
|
|
27
|
+
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
23
28
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
24
29
|
|
|
30
|
+
0.9.6 新增 AWS Lambda Event/DryRun、configuration 更新、JMESPath `--query`、
|
|
31
|
+
json/text/yaml 輸出、SQS FIFO/batch、SSM path/history、DynamoDB Binary
|
|
32
|
+
及 CloudWatch Logs。需要配套後端;Event 202 只表示持久化接受,並非 handler
|
|
33
|
+
執行完成。仍無完整 AWS CLI 自動分頁、table、Layers/Image/aliases 或全量 IAM。
|
|
34
|
+
|
|
35
|
+
0.9.5 新增 `gcloud-fufu scheduler`、`tasks`、`logging` 與 `bq-fufu`。
|
|
36
|
+
支援同租戶已登錄 HTTP 應用的排程/背景任務、BigQuery dataset/table、
|
|
37
|
+
insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Python
|
|
38
|
+
BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
|
|
39
|
+
安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
|
|
40
|
+
|
|
41
|
+
`fufu infrastructure` 提供 14 類 typed configuration mock。Compute Engine、
|
|
42
|
+
Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
|
|
43
|
+
VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
|
|
44
|
+
UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
|
|
45
|
+
新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gcloud-fufu scheduler jobs list
|
|
49
|
+
gcloud-fufu tasks queues list
|
|
50
|
+
bq-fufu query --use_legacy_sql=false 'SELECT 1 AS ready'
|
|
51
|
+
gcloud-fufu logging read 'severity>=ERROR'
|
|
52
|
+
fufu infrastructure catalog
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
|
|
56
|
+
外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
|
|
57
|
+
容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
|
|
58
|
+
留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
|
|
59
|
+
|
|
25
60
|
0.9.4 新增 GCS generation/metageneration 條件、`storage objects describe/update/list`、8 KiB 自訂 metadata 與二進位 `cat`。競爭寫入、過期條件、CLI metadata 和官方 gcloud 輸出欄位已對照驗收;不相符條件回傳 `PRECONDITION_FAILED`/HTTP 412。新功能需要同步更新 FUFU Console、GCS 儲存映像和 Cloud Shell。儲存升級前須停寫並備份完整 volume;新 metadata 格式可讀舊資料,回滾須還原升級前備份。
|
|
26
61
|
|
|
27
62
|
0.9.3 的常用輸出投影、清單篩選/排序/limit、Cloud Run 設定更新、Service Account 啟停及 Pub/Sub attributes/ACK deadline 持續保留。僅明列子集經過官方 gcloud、小用量真實 GCP 與隔離 Docker 驗收;不表示完整 Google IAM、SDK/ADC 或自動擴縮相容。
|
|
@@ -36,7 +71,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
36
71
|
## Cloudflare 前端部署與 R2
|
|
37
72
|
|
|
38
73
|
```bash
|
|
39
|
-
pip install 'fufu-cloud-cli==0.9.
|
|
74
|
+
pip install 'fufu-cloud-cli==0.9.6'
|
|
40
75
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
41
76
|
wrangler-fufu capabilities
|
|
42
77
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -80,7 +115,8 @@ pip install fufu-cloud-cli
|
|
|
80
115
|
fufu --version
|
|
81
116
|
```
|
|
82
117
|
|
|
83
|
-
npm
|
|
118
|
+
npm registry 尚未發布;以下是 registry 可用後的安裝語法。目前可安裝經驗收
|
|
119
|
+
的 npm tarball。npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
|
|
84
120
|
呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
|
|
85
121
|
不會自行下載 Python 或執行 pip。
|
|
86
122
|
|
|
@@ -97,12 +133,12 @@ npx --no-install aws-fufu --help
|
|
|
97
133
|
|
|
98
134
|
## 憑證與 CI
|
|
99
135
|
|
|
100
|
-
|
|
136
|
+
目前版本另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
|
|
101
137
|
Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
|
|
102
138
|
復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
|
|
103
139
|
已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
|
|
104
|
-
DIY backend,秘密欄位需外部 passphrase
|
|
105
|
-
|
|
140
|
+
DIY backend,秘密欄位需外部 passphrase。這些能力從 0.9.0 起納入 CLI,
|
|
141
|
+
仍需要配套服務端;不能把套件安裝成功當成支援驗收。
|
|
106
142
|
|
|
107
143
|
此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
|
|
108
144
|
管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
|
|
@@ -112,9 +148,9 @@ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
|
|
|
112
148
|
不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
|
|
113
149
|
與獨立 state,不可直接搬用 FUFU state。
|
|
114
150
|
|
|
115
|
-
|
|
151
|
+
目前版本另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
|
|
116
152
|
subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
|
|
117
|
-
|
|
153
|
+
CLI 安裝成功就推定服務端已具備。`gcloud-fufu pubsub` 提供建立、
|
|
118
154
|
查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
|
|
119
155
|
exactly-once、Function trigger 或保留期限保證。
|
|
120
156
|
|
|
@@ -206,10 +242,11 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
206
242
|
| --- | --- |
|
|
207
243
|
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
208
244
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
209
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP
|
|
245
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、configuration、RequestResponse/Event/DryRun、BUFFERED Function URL;ZIP 50 MiB/解壓 250 MiB,同步 6 MiB/Event 1 MiB;Event 為單機持久化重試,無 destinations/DLQ 或正式 autoscaling |
|
|
210
246
|
| GCS | bucket/object 子集、分段上傳/串流下載/binary cat、generation/metageneration 條件、metadata describe/update、前綴分頁;8 KiB custom metadata;無 versioning、signed URL、bucket IAM、一般 SDK 或 GCS IaC 資源 |
|
|
211
247
|
| S3 | bucket 與 object 子集;分段上傳/串流下載,受主機磁碟限制;已驗證超過 1 GiB,無 versioning / signed URL |
|
|
212
|
-
| DynamoDB / SQS / SSM |
|
|
248
|
+
| DynamoDB / SQS / SSM | 明列資料操作、Binary、FIFO group/dedup/batch、SSM 版本/path/history;無交易/TTL/Streams、SQS 背景 poller/DLQ/加密、KMS/SecureString |
|
|
249
|
+
| CloudWatch Logs | group/stream、put/get/filter events、retention 設定、CLI/UI/boto3;無 stdout 自動收集、Insights、subscriptions 或 retention 刪除保證 |
|
|
213
250
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
214
251
|
| API Gateway / Serverless profile | 簡化 HTTP Lambda proxy 和明確來源清單;不執行 plugins / CloudFormation |
|
|
215
252
|
| Azure | account 與資源群組目錄;尚無 Azure Functions 或其他 Azure runtime |
|
|
@@ -1,27 +1,39 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fufu-cloud-cli
|
|
3
|
-
Version: 0.9.4
|
|
4
|
-
Summary: FUFU Cloud CLI for tenant-scoped mock cloud development and deployment
|
|
5
|
-
License-Expression: LicenseRef-Proprietary
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: PyYAML==6.0.3
|
|
10
|
-
Requires-Dist: tomli==2.2.1; python_version < "3.11"
|
|
11
|
-
Provides-Extra: yaml
|
|
12
|
-
Requires-Dist: PyYAML==6.0.3; extra == "yaml"
|
|
13
|
-
Provides-Extra: test
|
|
14
|
-
Requires-Dist: boto3==1.40.66; extra == "test"
|
|
15
|
-
Provides-Extra: terraform
|
|
16
|
-
Requires-Dist: python-hcl2==7.3.1; extra == "terraform"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
1
|
# FUFU Cloud CLI
|
|
20
2
|
|
|
21
|
-
|
|
22
|
-
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
3
|
+
以六個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
|
|
4
|
+
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`、`bq-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
23
5
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
24
6
|
|
|
7
|
+
0.9.6 新增 AWS Lambda Event/DryRun、configuration 更新、JMESPath `--query`、
|
|
8
|
+
json/text/yaml 輸出、SQS FIFO/batch、SSM path/history、DynamoDB Binary
|
|
9
|
+
及 CloudWatch Logs。需要配套後端;Event 202 只表示持久化接受,並非 handler
|
|
10
|
+
執行完成。仍無完整 AWS CLI 自動分頁、table、Layers/Image/aliases 或全量 IAM。
|
|
11
|
+
|
|
12
|
+
0.9.5 新增 `gcloud-fufu scheduler`、`tasks`、`logging` 與 `bq-fufu`。
|
|
13
|
+
支援同租戶已登錄 HTTP 應用的排程/背景任務、BigQuery dataset/table、
|
|
14
|
+
insertAll/SELECT 與持久化日誌。`fufu_cloud_cli.gcp_sdk` 提供官方 Python
|
|
15
|
+
BigQuery、Cloud Tasks、Logging SDK 的明確 REST factory;可用 `[gcp]` extra
|
|
16
|
+
安裝已驗收相依。需要對應的新後端,並非透明 ADC 或所有 Google SDK 替換。
|
|
17
|
+
|
|
18
|
+
`fufu infrastructure` 提供 14 類 typed configuration mock。Compute Engine、
|
|
19
|
+
Cloud SQL、App Engine、DNS、VPC、Artifact Registry、Cloud Build、Gemini、
|
|
20
|
+
VM Manager、Eventarc 等設定不會啟動 VM/SQL/registry/網路/推論。
|
|
21
|
+
UI 顯示僅設定,CLI stderr 保留 MOCK ONLY,不因輸出投影而隱藏。
|
|
22
|
+
新服務尚未接入 Terraform/OpenTofu/Pulumi;單機背景投遞不是雲端 HA。
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gcloud-fufu scheduler jobs list
|
|
26
|
+
gcloud-fufu tasks queues list
|
|
27
|
+
bq-fufu query --use_legacy_sql=false 'SELECT 1 AS ready'
|
|
28
|
+
gcloud-fufu logging read 'severity>=ERROR'
|
|
29
|
+
fufu infrastructure catalog
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
BigQuery 是 tenant emulator 的 GoogleSQL 子集,無 DML/DDL、load/export、
|
|
33
|
+
外部表與效能/費用等價。Logging 保留每租戶最新 10,000 筆,不自動收集所有
|
|
34
|
+
容器 stdout。Tasks 每 queue 每秒最多 1 次、每租戶 2 個同時投遞;耗盡 task
|
|
35
|
+
留待檢查,與官方刪除策略不同。新的 dependency flow 不代表完整應用都能遷移。
|
|
36
|
+
|
|
25
37
|
0.9.4 新增 GCS generation/metageneration 條件、`storage objects describe/update/list`、8 KiB 自訂 metadata 與二進位 `cat`。競爭寫入、過期條件、CLI metadata 和官方 gcloud 輸出欄位已對照驗收;不相符條件回傳 `PRECONDITION_FAILED`/HTTP 412。新功能需要同步更新 FUFU Console、GCS 儲存映像和 Cloud Shell。儲存升級前須停寫並備份完整 volume;新 metadata 格式可讀舊資料,回滾須還原升級前備份。
|
|
26
38
|
|
|
27
39
|
0.9.3 的常用輸出投影、清單篩選/排序/limit、Cloud Run 設定更新、Service Account 啟停及 Pub/Sub attributes/ACK deadline 持續保留。僅明列子集經過官方 gcloud、小用量真實 GCP 與隔離 Docker 驗收;不表示完整 Google IAM、SDK/ADC 或自動擴縮相容。
|
|
@@ -36,7 +48,7 @@ Python 預設識別而遇到 Cloudflare HTTP 403/1010。識別標頭不取代
|
|
|
36
48
|
## Cloudflare 前端部署與 R2
|
|
37
49
|
|
|
38
50
|
```bash
|
|
39
|
-
pip install 'fufu-cloud-cli==0.9.
|
|
51
|
+
pip install 'fufu-cloud-cli==0.9.6'
|
|
40
52
|
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
41
53
|
wrangler-fufu capabilities
|
|
42
54
|
wrangler-fufu pages project create frontend --production-branch main
|
|
@@ -80,7 +92,8 @@ pip install fufu-cloud-cli
|
|
|
80
92
|
fufu --version
|
|
81
93
|
```
|
|
82
94
|
|
|
83
|
-
npm
|
|
95
|
+
npm registry 尚未發布;以下是 registry 可用後的安裝語法。目前可安裝經驗收
|
|
96
|
+
的 npm tarball。npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
|
|
84
97
|
呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
|
|
85
98
|
不會自行下載 Python 或執行 pip。
|
|
86
99
|
|
|
@@ -97,12 +110,12 @@ npx --no-install aws-fufu --help
|
|
|
97
110
|
|
|
98
111
|
## 憑證與 CI
|
|
99
112
|
|
|
100
|
-
|
|
113
|
+
目前版本另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
|
|
101
114
|
Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
|
|
102
115
|
復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
|
|
103
116
|
已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
|
|
104
|
-
DIY backend,秘密欄位需外部 passphrase
|
|
105
|
-
|
|
117
|
+
DIY backend,秘密欄位需外部 passphrase。這些能力從 0.9.0 起納入 CLI,
|
|
118
|
+
仍需要配套服務端;不能把套件安裝成功當成支援驗收。
|
|
106
119
|
|
|
107
120
|
此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
|
|
108
121
|
管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
|
|
@@ -112,9 +125,9 @@ GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
|
|
|
112
125
|
不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
|
|
113
126
|
與獨立 state,不可直接搬用 FUFU state。
|
|
114
127
|
|
|
115
|
-
|
|
128
|
+
目前版本另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
|
|
116
129
|
subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
|
|
117
|
-
|
|
130
|
+
CLI 安裝成功就推定服務端已具備。`gcloud-fufu pubsub` 提供建立、
|
|
118
131
|
查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
|
|
119
132
|
exactly-once、Function trigger 或保留期限保證。
|
|
120
133
|
|
|
@@ -206,10 +219,11 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
206
219
|
| --- | --- |
|
|
207
220
|
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
208
221
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
209
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP
|
|
222
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、configuration、RequestResponse/Event/DryRun、BUFFERED Function URL;ZIP 50 MiB/解壓 250 MiB,同步 6 MiB/Event 1 MiB;Event 為單機持久化重試,無 destinations/DLQ 或正式 autoscaling |
|
|
210
223
|
| GCS | bucket/object 子集、分段上傳/串流下載/binary cat、generation/metageneration 條件、metadata describe/update、前綴分頁;8 KiB custom metadata;無 versioning、signed URL、bucket IAM、一般 SDK 或 GCS IaC 資源 |
|
|
211
224
|
| S3 | bucket 與 object 子集;分段上傳/串流下載,受主機磁碟限制;已驗證超過 1 GiB,無 versioning / signed URL |
|
|
212
|
-
| DynamoDB / SQS / SSM |
|
|
225
|
+
| DynamoDB / SQS / SSM | 明列資料操作、Binary、FIFO group/dedup/batch、SSM 版本/path/history;無交易/TTL/Streams、SQS 背景 poller/DLQ/加密、KMS/SecureString |
|
|
226
|
+
| CloudWatch Logs | group/stream、put/get/filter events、retention 設定、CLI/UI/boto3;無 stdout 自動收集、Insights、subscriptions 或 retention 刪除保證 |
|
|
213
227
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
214
228
|
| API Gateway / Serverless profile | 簡化 HTTP Lambda proxy 和明確來源清單;不執行 plugins / CloudFormation |
|
|
215
229
|
| Azure | account 與資源群組目錄;尚無 Azure Functions 或其他 Azure runtime |
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fufu-cloud-cli"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.6"
|
|
8
8
|
description = "FUFU Cloud CLI for tenant-scoped mock cloud development and deployment"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
-
dependencies = ["PyYAML==6.0.3", "tomli==2.2.1; python_version<'3.11'"]
|
|
11
|
+
dependencies = ["PyYAML==6.0.3", "jmespath==1.1.0", "tomli==2.2.1; python_version<'3.11'"]
|
|
12
12
|
license = "LicenseRef-Proprietary"
|
|
13
13
|
license-files = ["LICENSE"]
|
|
14
14
|
|
|
@@ -16,6 +16,7 @@ license-files = ["LICENSE"]
|
|
|
16
16
|
yaml = ["PyYAML==6.0.3"]
|
|
17
17
|
test = ["boto3==1.40.66"]
|
|
18
18
|
terraform = ["python-hcl2==7.3.1"]
|
|
19
|
+
gcp = ["google-cloud-bigquery==3.38.0", "google-cloud-tasks==2.20.0", "google-cloud-logging==3.12.1"]
|
|
19
20
|
|
|
20
21
|
[project.scripts]
|
|
21
22
|
fufu = "fufu_cloud_cli.cli:main"
|
|
@@ -23,9 +24,10 @@ aws-fufu = "fufu_cloud_cli.cli:aws_main"
|
|
|
23
24
|
gcloud-fufu = "fufu_cloud_cli.cli:gcloud_main"
|
|
24
25
|
az-fufu = "fufu_cloud_cli.cli:az_main"
|
|
25
26
|
wrangler-fufu = "fufu_cloud_cli.cli:wrangler_main"
|
|
27
|
+
bq-fufu = "fufu_cloud_cli.cli:bq_main"
|
|
26
28
|
|
|
27
29
|
[tool.setuptools.packages.find]
|
|
28
30
|
where = ["src"]
|
|
29
31
|
|
|
30
32
|
[tool.setuptools.package-data]
|
|
31
|
-
fufu_cloud_cli = ["capabilities.json", "pulumi-endpoints.json", "runtime/*/Dockerfile", "runtime/ingress/proxy.py"]
|
|
33
|
+
fufu_cloud_cli = ["capabilities.json", "pulumi-endpoints.json", "runtime/*/Dockerfile", "runtime/ingress/proxy.py", "vendor/AWS-CLI-LICENSE.txt"]
|
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
from .cli import entry
|
|
4
4
|
|
|
5
5
|
if __name__ == "__main__":
|
|
6
|
-
if len(sys.argv) > 1 and sys.argv[1] in {"fufu", "aws-fufu", "gcloud-fufu", "az-fufu"}:
|
|
6
|
+
if len(sys.argv) > 1 and sys.argv[1] in {"fufu", "aws-fufu", "gcloud-fufu", "az-fufu", "wrangler-fufu", "bq-fufu"}:
|
|
7
7
|
entry(sys.argv[1], sys.argv[2:])
|
|
8
8
|
else:
|
|
9
9
|
entry("fufu", sys.argv[1:])
|
|
@@ -184,6 +184,14 @@ class ApiClient:
|
|
|
184
184
|
return json.loads(raw)
|
|
185
185
|
except HTTPError as exc:
|
|
186
186
|
status = exc.code
|
|
187
|
+
if self.provider == 'aws' and '/control/native' in path:
|
|
188
|
+
from .aws_errors import AWS_ERRORS
|
|
189
|
+
try:
|
|
190
|
+
value = json.loads(exc.read(4096))
|
|
191
|
+
code = value.get('code') if isinstance(value, dict) else None
|
|
192
|
+
if code in AWS_ERRORS: raise FufuError(code, 'AWS 操作遭拒;未輸出原始錯誤內容。') from None
|
|
193
|
+
except (ValueError, OSError): pass
|
|
194
|
+
finally: exc.close()
|
|
187
195
|
exc.close()
|
|
188
196
|
codes = {401: "AUTHENTICATION_FAILED", 403: "ACCESS_DENIED", 404: "RESOURCE_NOT_FOUND", 409: "RESOURCE_CONFLICT", 410: "UPLOAD_EXPIRED", 412: "PRECONDITION_FAILED", 413: "PAYLOAD_LIMIT", 429: "RATE_LIMITED", 507: "STORAGE_CAPACITY"}
|
|
189
197
|
if status == 507: raise FufuError('STORAGE_CAPACITY', 'FUFU 主機磁碟容量不足;這是目前可用空間的限制,並非雲端物件大小上限。') from None
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Only documented provider error codes may cross the public transport boundary."""
|
|
2
|
+
AWS_ERRORS = frozenset('''ConditionalCheckFailedException ResourceNotFoundException ResourceInUseException ValidationException
|
|
3
|
+
NoSuchKey NoSuchBucket NotFound NotModified ParameterNotFound ParameterAlreadyExists ParameterVersionNotFound QueueDoesNotExist
|
|
4
|
+
PreconditionFailed InvalidRange InvalidParameterException InvalidParameterValue InvalidAttributeName InvalidAttributeValue QueueNameExists
|
|
5
|
+
PurgeQueueInProgress ReceiptHandleIsInvalid BatchEntryIdsNotDistinct EmptyBatchRequest BatchRequestTooLong InvalidBatchEntryId
|
|
6
|
+
TooManyEntriesInBatchRequest InvalidMessageContents AWS.SimpleQueueService.NonExistentQueue TooManyRequestsException InvalidRequestContentException'''.split())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""AWS JMESPath and native text formatting over the supported response subset."""
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import jmespath
|
|
6
|
+
from jmespath.exceptions import JMESPathError
|
|
7
|
+
|
|
8
|
+
from .errors import FufuError, require
|
|
9
|
+
from .vendor.aws_text import format_text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def expression(value):
|
|
13
|
+
require(isinstance(value, str) and 0 < len(value) <= 65536,
|
|
14
|
+
'INVALID_QUERY', 'AWS query 必須為非空 JMESPath 運算式,最多 65536 字元。')
|
|
15
|
+
try:
|
|
16
|
+
return jmespath.compile(value)
|
|
17
|
+
except JMESPathError:
|
|
18
|
+
raise FufuError('INVALID_QUERY', '無效的 AWS JMESPath query;未執行操作。', 2) from None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def configuration(value):
|
|
22
|
+
if 'FunctionName' in value:
|
|
23
|
+
return value
|
|
24
|
+
resources = value.get('resources', {})
|
|
25
|
+
fields = {'FunctionName': value.get('name'), 'FunctionArn': value.get('functionArn'),
|
|
26
|
+
'Runtime': value.get('runtime'), 'Handler': value.get('handler'), 'Role': value.get('role'),
|
|
27
|
+
'MemorySize': resources.get('memoryMiB'), 'Timeout': resources.get('timeoutSeconds'),
|
|
28
|
+
'RevisionId': value.get('revision'), 'LastModified': value.get('createdAt')}
|
|
29
|
+
result = {key: item for key, item in fields.items() if item is not None}
|
|
30
|
+
if resources.get('tmpMiB') is not None: result['EphemeralStorage'] = {'Size': resources['tmpMiB']}
|
|
31
|
+
if value.get('status') == 'running': result.update(State='Active', LastUpdateStatus='Successful')
|
|
32
|
+
if 'env' in value: result['Environment'] = {'Variables': value['env']}
|
|
33
|
+
result['Fufu'] = {key: value[key] for key in ['mode', 'backend', 'readiness', 'serviceId', 'publicAccess', 'iamEnforced'] if key in value}
|
|
34
|
+
result['Fufu'].setdefault('mode', 'simplified')
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def native(value, argv):
|
|
39
|
+
if argv[:1] != ['lambda'] or len(argv) < 2 or not isinstance(value, dict):
|
|
40
|
+
return value
|
|
41
|
+
action = argv[1]
|
|
42
|
+
if action == 'list-functions':
|
|
43
|
+
return {**value, 'Functions': [configuration(item) for item in value.get('Functions', [])]}
|
|
44
|
+
if action == 'get-function':
|
|
45
|
+
return {'Configuration': configuration(value), 'Fufu': {'mode': 'simplified', 'codeDownloadAvailable': False}}
|
|
46
|
+
if action in {'create-function', 'update-function-code', 'get-function-configuration', 'update-function-configuration'}:
|
|
47
|
+
return configuration(value)
|
|
48
|
+
if action == 'delete-function': return None
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render(value, output='json', query=None):
|
|
53
|
+
require(output in {'json', 'text', 'yaml', 'off'}, 'UNSUPPORTED_FORMAT',
|
|
54
|
+
'AWS 輸出支援 json、text、yaml、off;table 與 yaml-stream 尚未支援。')
|
|
55
|
+
try:
|
|
56
|
+
value = query.search(value) if query else value
|
|
57
|
+
except JMESPathError:
|
|
58
|
+
raise FufuError('INVALID_QUERY', 'AWS query 無法套用到回應;操作可能已完成,請先查詢資源狀態。', 2) from None
|
|
59
|
+
if output == 'off': return ''
|
|
60
|
+
if output == 'text':
|
|
61
|
+
stream = io.StringIO(); format_text(value, stream); return stream.getvalue()
|
|
62
|
+
if output == 'yaml':
|
|
63
|
+
import yaml
|
|
64
|
+
return yaml.safe_dump(value, allow_unicode=True, sort_keys=False)
|
|
65
|
+
return json.dumps(value, ensure_ascii=False, indent=4) + '\n'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""bq command subset. Uses the same IAM-enforced BigQuery API as the console."""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .api_backend import ApiClient
|
|
6
|
+
from .errors import require
|
|
7
|
+
from .gcp_infra import call
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def reference(value, project):
|
|
11
|
+
if ':' in value: selected, value = value.split(':', 1)
|
|
12
|
+
else: selected = project
|
|
13
|
+
require(selected == project, 'ACCESS_DENIED', 'BigQuery project 必須符合 credential scope。')
|
|
14
|
+
parts = value.split('.')
|
|
15
|
+
require(len(parts) <= 2 and all(parts), 'INVALID_RESOURCE', '請使用 project:dataset.table。')
|
|
16
|
+
return {'projectId': project, 'datasetId': parts[0], **({'tableId': parts[1]} if len(parts) == 2 else {})}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def schema(value):
|
|
20
|
+
path = Path(value)
|
|
21
|
+
if path.is_file():
|
|
22
|
+
require(path.stat().st_size <= 1024**2, 'PAYLOAD_LIMIT', 'Schema 檔案超過 1 MiB。')
|
|
23
|
+
result = json.loads(path.read_text())
|
|
24
|
+
return result if isinstance(result, dict) else {'fields': result}
|
|
25
|
+
return {'fields': [{'name': name, 'type': kind.upper()} for name, kind in (field.split(':', 1) for field in value.split(','))]}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def command(argv, Parser):
|
|
29
|
+
# bq permits global flags before or after the command.
|
|
30
|
+
project = location = None; clean = []; i = 0
|
|
31
|
+
while i < len(argv):
|
|
32
|
+
flag, sep, inline = argv[i].partition('=')
|
|
33
|
+
if flag in {'--project_id', '--location', '--format'}:
|
|
34
|
+
value = inline if sep else (argv[i + 1] if i + 1 < len(argv) else '')
|
|
35
|
+
require(value and not value.startswith('--'), 'INVALID_ARGUMENT', '全域參數缺少值。')
|
|
36
|
+
if flag == '--project_id': project = value
|
|
37
|
+
elif flag == '--location': location = value
|
|
38
|
+
else: require(value in {'json', 'prettyjson'}, 'UNSUPPORTED_FORMAT', 'bq-fufu 輸出支援 json 與 prettyjson。')
|
|
39
|
+
i += 1 if sep else 2
|
|
40
|
+
elif flag in {'--quiet', '-q', '--headless'}: i += 1
|
|
41
|
+
else: clean.append(argv[i]); i += 1
|
|
42
|
+
p = Parser(prog='bq-fufu', description='BigQuery dataset/table、insertAll 與 SELECT 子集。未支援 load jobs、DML/DDL、external tables 或成本估算。')
|
|
43
|
+
actions = p.add_subparsers(dest='action', required=True)
|
|
44
|
+
for action in ['mk', 'ls', 'show', 'rm', 'head']:
|
|
45
|
+
q = actions.add_parser(action)
|
|
46
|
+
q.add_argument('resource', nargs='?' if action == 'ls' else None)
|
|
47
|
+
q.add_argument('--dataset', '-d', action='store_true'); q.add_argument('--table', '-t', action='store_true')
|
|
48
|
+
if action == 'mk': q.add_argument('schema', nargs='?'); q.add_argument('--description')
|
|
49
|
+
if action == 'rm': q.add_argument('--recursive', '-r', action='store_true'); q.add_argument('--force', '-f', action='store_true')
|
|
50
|
+
if action in {'head', 'ls'}: q.add_argument('--max_results', '-n', type=int, default=100); q.add_argument('--page_token')
|
|
51
|
+
q = actions.add_parser('query'); q.add_argument('query'); q.add_argument('--use_legacy_sql', choices=['false'], default='false'); q.add_argument('--nouse_legacy_sql', action='store_true'); q.add_argument('--parameter', action='append', default=[]); q.add_argument('--max_rows', type=int, default=100)
|
|
52
|
+
q = actions.add_parser('insert'); q.add_argument('resource'); q.add_argument('file')
|
|
53
|
+
a = p.parse_args(clean)
|
|
54
|
+
sys.stderr.write('FUFU BigQuery subset: tenant emulator; unsupported SQL and job types fail explicitly.\n')
|
|
55
|
+
with ApiClient('gcp', read_only=a.action in {'ls', 'show', 'head'}) as api:
|
|
56
|
+
selected = api.whoami()['projectId']
|
|
57
|
+
require(project is None or project == selected, 'ACCESS_DENIED', 'BigQuery project 必須符合 credential scope。')
|
|
58
|
+
if a.action == 'query':
|
|
59
|
+
params = {'projectId': selected, 'query': a.query, 'useLegacySql': False, 'maxResults': a.max_rows}
|
|
60
|
+
if location: params['location'] = location
|
|
61
|
+
if a.parameter:
|
|
62
|
+
params['parameterMode'] = 'NAMED'; params['queryParameters'] = []
|
|
63
|
+
for value in a.parameter:
|
|
64
|
+
parts = value.split(':', 2); require(len(parts) == 3, 'INVALID_ARGUMENT', 'parameter 請使用 name:type:value。')
|
|
65
|
+
name, kind, data = parts
|
|
66
|
+
params['queryParameters'].append({'name': name, 'parameterType': {'type': kind.upper() or 'STRING'}, 'parameterValue': {'value': data}})
|
|
67
|
+
return call(api, 'bigquery', 'queries.query', params)
|
|
68
|
+
ref = reference(a.resource, selected) if a.resource else {'projectId': selected}
|
|
69
|
+
table = 'tableId' in ref
|
|
70
|
+
if getattr(a, 'dataset', False): require(not table, 'INVALID_RESOURCE', 'Dataset 命令不可傳入 table。')
|
|
71
|
+
if getattr(a, 'table', False): require(table, 'INVALID_RESOURCE', 'Table 命令需要 dataset.table。')
|
|
72
|
+
group = 'tables' if table else 'datasets'
|
|
73
|
+
if a.action == 'mk':
|
|
74
|
+
require(not table or a.schema, 'INVALID_ARGUMENT', 'Table 建立需要 schema。')
|
|
75
|
+
value = {'tableReference' if table else 'datasetReference': ref}
|
|
76
|
+
if table: value['schema'] = schema(a.schema)
|
|
77
|
+
elif location: value['location'] = location
|
|
78
|
+
if a.description is not None: value['description'] = a.description
|
|
79
|
+
return call(api, 'bigquery', group + '.insert', {'projectId': selected, **({'datasetId': ref['datasetId']} if table else {}), 'table' if table else 'dataset': value})
|
|
80
|
+
if a.action == 'rm': return call(api, 'bigquery', group + '.delete', {**ref, **({'deleteContents': a.recursive} if not table else {})})
|
|
81
|
+
if a.action == 'show': return call(api, 'bigquery', group + '.get', ref)
|
|
82
|
+
if a.action in {'ls', 'head'}:
|
|
83
|
+
require(a.action != 'head' or table, 'INVALID_RESOURCE', 'head 需要 table。')
|
|
84
|
+
require(a.action != 'ls' or not table, 'INVALID_RESOURCE', 'ls 請指定 dataset。')
|
|
85
|
+
op = 'tabledata.list' if table else 'tables.list' if 'datasetId' in ref else 'datasets.list'
|
|
86
|
+
return call(api, 'bigquery', op, {**ref, 'maxResults': a.max_results, **({'pageToken': a.page_token} if a.page_token else {})})
|
|
87
|
+
require(table, 'INVALID_RESOURCE', 'insert 需要 table。')
|
|
88
|
+
file = Path(a.file); require(file.is_file() and file.stat().st_size <= 10 * 1024**2, 'PAYLOAD_LIMIT', 'NDJSON 檔案超過 10 MiB。')
|
|
89
|
+
rows = [{'json': json.loads(line)} for line in file.read_text().splitlines() if line.strip()]
|
|
90
|
+
result = call(api, 'bigquery', 'tabledata.insertAll', {**ref, 'rows': rows})
|
|
91
|
+
require(not result.get('insertErrors'), 'BIGQUERY_INSERT_FAILED', 'BigQuery 拒絕部分資料列;請檢查 schema。')
|
|
92
|
+
return result
|