From bafad214e1baf59c5c1d27f77adb22753d86af7d Mon Sep 17 00:00:00 2001 From: "ming.guo" Date: Thu, 19 Mar 2026 16:28:31 +0800 Subject: [PATCH] update sdk --- appstore/01_get_classify_list.py | 25 ++++ appstore/02_get_terminal_list.py | 23 ++++ appstore/03_get_language_list.py | 23 ++++ appstore/04_upload_apk.py | 32 +++++ appstore/05_upload_image.py | 37 ++++++ appstore/06_create_app.py | 62 ++++++++++ appstore/07_get_app_detail.py | 26 +++++ appstore/08_update_app_detail.py | 45 ++++++++ appstore/09_upgrade_app_version.py | 41 +++++++ appstore/10_get_audit_result.py | 29 +++++ appstore/11_get_app_newest_version.py | 30 +++++ appstore/12_update_app_version_detail.py | 39 +++++++ appstore/13_remove_app.py | 26 +++++ appstore/README.md | 77 +++++++++++++ appstore/__init__.py | 3 + appstore/client.py | 141 +++++++++++++++++++++++ appstore/requirements.txt | 2 + 17 files changed, 661 insertions(+) create mode 100644 appstore/01_get_classify_list.py create mode 100644 appstore/02_get_terminal_list.py create mode 100644 appstore/03_get_language_list.py create mode 100644 appstore/04_upload_apk.py create mode 100644 appstore/05_upload_image.py create mode 100644 appstore/06_create_app.py create mode 100644 appstore/07_get_app_detail.py create mode 100644 appstore/08_update_app_detail.py create mode 100644 appstore/09_upgrade_app_version.py create mode 100644 appstore/10_get_audit_result.py create mode 100644 appstore/11_get_app_newest_version.py create mode 100644 appstore/12_update_app_version_detail.py create mode 100644 appstore/13_remove_app.py create mode 100644 appstore/README.md create mode 100644 appstore/__init__.py create mode 100644 appstore/client.py create mode 100644 appstore/requirements.txt diff --git a/appstore/01_get_classify_list.py b/appstore/01_get_classify_list.py new file mode 100644 index 0000000..9f43845 --- /dev/null +++ b/appstore/01_get_classify_list.py @@ -0,0 +1,25 @@ +""" +Get App Category List + +Retrieve the list of supported app categories for use when creating an app. + +API: POST /v2/appstore/appstore/app/getClassifyList +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmimeghjk546 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmimeghjk546 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/getClassifyList", { + "lan_type": 1, # 1: Chinese, 2: English +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/02_get_terminal_list.py b/appstore/02_get_terminal_list.py new file mode 100644 index 0000000..e163c2a --- /dev/null +++ b/appstore/02_get_terminal_list.py @@ -0,0 +1,23 @@ +""" +Get Terminal List + +Retrieve the list of supported device terminal models for use when creating or updating an app. + +API: POST /v2/appstore/appstore/app/getTerminalList +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmifeghjk535 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmifeghjk535 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/getTerminalList", {}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/03_get_language_list.py b/appstore/03_get_language_list.py new file mode 100644 index 0000000..5727778 --- /dev/null +++ b/appstore/03_get_language_list.py @@ -0,0 +1,23 @@ +""" +Get Language List + +Retrieve the list of country language codes for configuring multilingual app names and descriptions. + +API: POST /v2/appstore/appstore/app/getLanguageList +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmideghjk524 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmideghjk524 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/getLanguageList", {}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/04_upload_apk.py b/appstore/04_upload_apk.py new file mode 100644 index 0000000..fdcfa80 --- /dev/null +++ b/appstore/04_upload_apk.py @@ -0,0 +1,32 @@ +""" +Upload APK + +Upload an APK file to obtain a resource UUID, which is required for creating +or upgrading an app. + +API: POST /v2/midplat/filecore/file/uploadApk +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmireghjk568 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmireghjk568 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +file_path = "your_app.apk" + +params = { + "md5": SunmiAppStoreClient.calculate_md5(file_path), + "file_type_key": "appstore_apk", +} + +response = client.upload("/v2/midplat/filecore/file/uploadApk", file_path, params) + +print(json.dumps(response, indent=4, ensure_ascii=False)) +print("apk_uuid =", response.get("data", {}).get("uuid", "")) diff --git a/appstore/05_upload_image.py b/appstore/05_upload_image.py new file mode 100644 index 0000000..b196da0 --- /dev/null +++ b/appstore/05_upload_image.py @@ -0,0 +1,37 @@ +""" +Upload Image + +Upload an image file (icon, vertical screenshot, or horizontal screenshot) to +obtain a resource UUID, which is required for creating or updating an app. + +API: POST /v2/midplat/filecore/file/uploadImage +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmizeghjk557 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmizeghjk557 + +file_type_key options: + appstore_icon - App icon + appstore_vscreenshot - Vertical screenshot + appstore_hscreenshot - Horizontal screenshot +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +file_path = "your_image.png" + +params = { + "md5": SunmiAppStoreClient.calculate_md5(file_path), + "file_type_key": "appstore_icon", # appstore_icon / appstore_vscreenshot / appstore_hscreenshot +} + +response = client.upload("/v2/midplat/filecore/file/uploadImage", file_path, params) + +print(json.dumps(response, indent=4, ensure_ascii=False)) +print("image_uuid =", response.get("data", {}).get("uuid", "")) diff --git a/appstore/06_create_app.py b/appstore/06_create_app.py new file mode 100644 index 0000000..da500d2 --- /dev/null +++ b/appstore/06_create_app.py @@ -0,0 +1,62 @@ +""" +Create App + +Create a new app on the Sunmi AppStore. Before calling this API, you need to: + 1. Upload the APK -> get apk_uuid (04_upload_apk.py) + 2. Upload icon/images -> get icon/image UUIDs (05_upload_image.py) + 3. Get category list -> get cf_id (01_get_classify_list.py) + 4. Get terminal list -> get terminal names (02_get_terminal_list.py) + 5. Get language list -> get lan_id (03_get_language_list.py) + +API: POST /v2/appstore/appstore/app/createApp +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmrfeghjk535 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmrfeghjk535 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/createApp", { + "app_name": "Your App Name", + "icon_url_uuid": "YOUR_ICON_UUID", + "pic_vertical_screen_uuid": [ + "YOUR_SCREENSHOT_UUID_1", + "YOUR_SCREENSHOT_UUID_2", + "YOUR_SCREENSHOT_UUID_3", + ], + # "pic_horizontal_screen_uuid": ["YOUR_HSCREENSHOT_UUID"], # optional + "apk_uuid": "YOUR_APK_UUID", + "app_introduction": "A brief description of your app (10-1000 characters).", + "cf_id": "YOUR_CATEGORY_ID", # from Get App Category List API + "terminals": ["P2", "V3"], # from Get Terminal List API + "area": [1, 2, 3], # 1: Mainland China, 2: HK/MO/TW, 3: Overseas + "range": 0, # 0: visible to all, 1: visible to own channel only + "deployment_type": 1, # 1: full deployment, 2: gray deployment + "language": [ # multilingual app names (optional) + {"lan_id": "YOUR_LAN_ID", "name": "App Name in English"}, + ], + "language_introduction": [ # multilingual descriptions (optional) + {"lan_id": "YOUR_LAN_ID", "introduction": "App description in English"}, + ], + "remarks": "Remarks for the reviewer (10-200 characters).", + # "notify_url": "https://example.com/your-callback", # optional audit result callback URL + "pond_type": 0, # 0: public store, 1: private store + + # --- Gray deployment params (only when deployment_type=2) --- + # "gray_msn_list": ["MSN1", "MSN2"], + # "gray_version": 0, # 0: default gray, 2: percentage-based gray + # "gray_ppm": 10000, # gray ratio, 10000 = 1% (only when gray_version=2) + # "gray_entity_id_list": ["entity1"], + # "gray_start_time": 1700000000, # unix timestamp + # "gray_time_zone": "Asia/Shanghai", + # "deploy_location_id_list": ["CN"], +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/07_get_app_detail.py b/appstore/07_get_app_detail.py new file mode 100644 index 0000000..3a4cbff --- /dev/null +++ b/appstore/07_get_app_detail.py @@ -0,0 +1,26 @@ +""" +Get App Detail + +Query the basic detail configuration of an app by its package name. + +API: POST /v2/appstore/appstore/app/getAppDetail +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmrzeghjk557 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmrzeghjk557 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/getAppDetail", { + "package_name": "com.example.yourapp", + "pond_type": 0, # 0: public store, 1: private store +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/08_update_app_detail.py b/appstore/08_update_app_detail.py new file mode 100644 index 0000000..660a09d --- /dev/null +++ b/appstore/08_update_app_detail.py @@ -0,0 +1,45 @@ +""" +Update App Detail + +Update basic information of an existing app such as introduction, screenshots, +compatible terminals, and distribution regions. Changes require a new review. + +API: POST /v2/appstore/appstore/app/updateAppDetail +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmiqeghjk513 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmiqeghjk513 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/updateAppDetail", { + "package_name": "com.example.yourapp", + "language": [ + {"lan_id": "YOUR_LAN_ID", "name": "Updated App Name"}, + ], + "app_introduction": "Updated app description (10-1000 characters).", + "language_introduction": [ + {"lan_id": "YOUR_LAN_ID", "introduction": "Updated description in English"}, + ], + "terminals": ["P2", "V3"], + "pic_vertical_screen_uuid": [ + "YOUR_SCREENSHOT_UUID_1", + "YOUR_SCREENSHOT_UUID_2", + "YOUR_SCREENSHOT_UUID_3", + ], + # "pic_horizontal_screen_uuid": ["YOUR_HSCREENSHOT_UUID"], + "area": [1, 2, 3], + "range": 0, + "icon_url_uuid": "YOUR_ICON_UUID", + # "notify_url": "https://example.com/your-callback", + "pond_type": 0, +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/09_upgrade_app_version.py b/appstore/09_upgrade_app_version.py new file mode 100644 index 0000000..7f30d3f --- /dev/null +++ b/appstore/09_upgrade_app_version.py @@ -0,0 +1,41 @@ +""" +Upgrade App Version + +Upgrade the version of an audited app. Supports both full and gray deployment. +Before calling this API, upload the new APK via the Upload APK API first. + +API: POST /v2/appstore/appstore/app/upgradeAppVersion +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmiceghjk502 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmiceghjk502 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/upgradeAppVersion", { + "package_name": "com.example.yourapp", + "remarks": "Remarks for the reviewer (10-200 characters).", + "update_content": "What's new in this version (3-2500 characters).", + "update_flag": 1, # 1: full release, 2: gray release + "apk_uuid": "YOUR_APK_UUID", # UUID from Upload APK API + # "notify_url": "https://example.com/your-callback", + "pond_type": 0, # 0: public store, 1: private store + + # --- Gray deployment params (only when update_flag=2) --- + # "gray_msn_list": ["MSN1", "MSN2"], + # "gray_version": 0, + # "gray_ppm": 10000, + # "gray_entity_id_list": ["entity1"], + # "gray_start_time": 1700000000, + # "gray_time_zone": "Asia/Shanghai", + # "deploy_location_id_list": ["CN"], +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/10_get_audit_result.py b/appstore/10_get_audit_result.py new file mode 100644 index 0000000..92849cf --- /dev/null +++ b/appstore/10_get_audit_result.py @@ -0,0 +1,29 @@ +""" +Get Audit Result + +Batch query the audit status and results for app creation, version upgrade, +or detail modification. Supports up to 100 package names per request. + +API: POST /v2/appstore/appstore/app/getAuditResult +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmrmeghjk546 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmrmeghjk546 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/getAuditResult", { + "package_name_list": [ + "com.example.yourapp", + ], + "pond_type": 0, # 0: public store, 1: private store +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/11_get_app_newest_version.py b/appstore/11_get_app_newest_version.py new file mode 100644 index 0000000..8b8e261 --- /dev/null +++ b/appstore/11_get_app_newest_version.py @@ -0,0 +1,30 @@ +""" +Get App Newest Version Information + +Query the latest formal and gray version data of an app. Supports fetching +historical version information with pagination. + +API: POST /v2/appstore/appstore/app/getAppNewestVersionInfo +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmrreghjk568 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmrreghjk568 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/getAppNewestVersionInfo", { + "package_name": "com.example.yourapp", + "pond_type": 0, # 0: public store, 1: private store + "get_extend_version_info": True, # whether to include historical version info + "page_num": 1, + "page_size": 10, +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/12_update_app_version_detail.py b/appstore/12_update_app_version_detail.py new file mode 100644 index 0000000..89dd534 --- /dev/null +++ b/appstore/12_update_app_version_detail.py @@ -0,0 +1,39 @@ +""" +Update Version Deployment Detail + +Switch deployment mode (e.g. gray to full), adjust gray MSN list and gray status +for a specific version of an app. + +API: POST /v2/appstore/appstore/app/updateAppVersionDetail +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmixeghjk491 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmixeghjk491 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/updateAppVersionDetail", { + "package_name": "com.example.yourapp", + "version_code": 1, # target version code + "deployment_type": 1, # 1: full deployment, 2: gray deployment + "pond_type": 0, # 0: public store, 1: private store + + # --- Gray deployment params (only when deployment_type=2) --- + # "gray_msn_list": ["MSN1", "MSN2"], + # "gray_status": 1, # 1: enable gray, 2: disable gray + # "gray_version": 0, + # "gray_ppm": 10000, + # "gray_entity_id_list": ["entity1"], + # "gray_start_time": 1700000000, + # "deploy_location_id_list": ["CN"], + # "ignore_invalid_gray_msn": 0, # 0: do not ignore, 1: ignore invalid MSNs +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/13_remove_app.py b/appstore/13_remove_app.py new file mode 100644 index 0000000..564e385 --- /dev/null +++ b/appstore/13_remove_app.py @@ -0,0 +1,26 @@ +""" +Remove App + +Delete an app from the channel on the Sunmi AppStore. + +API: POST /v2/appstore/appstore/app/removeApp +Doc (zh-CN): https://developer.sunmi.com/docs/zh-CN/cdixeghjk491/xmrieghjk579 +Doc (en-US): https://developer.sunmi.com/docs/en-US/cdixeghjk491/xmrieghjk579 +""" + +import json +import os + +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +response = client.post("/v2/appstore/appstore/app/removeApp", { + "package_name": "com.example.yourapp", + "pond_type": 0, # 0: public store, 1: private store +}) + +print(json.dumps(response, indent=4, ensure_ascii=False)) diff --git a/appstore/README.md b/appstore/README.md new file mode 100644 index 0000000..647e66f --- /dev/null +++ b/appstore/README.md @@ -0,0 +1,77 @@ +# Sunmi AppStore OpenAPI Python SDK + +A lightweight Python SDK for the [Sunmi AppStore OpenAPI](https://developer.sunmi.com/docs/en-US/cdixeghjk491/faceghjk502), providing authentication, request signing, and ready-to-use examples for every API endpoint. + +## Prerequisites + +- Python 3.7+ +- A Sunmi OpenAPI **App ID** and **App Key** (obtain from the [Sunmi Developer Portal](https://developer.sunmi.com)) + +## Installation + +```bash +pip3 install -r requirements.txt +``` + +## Quick Start + +1. Set your credentials as environment variables: + +```bash +export SUNMI_APP_ID="your_app_id" +export SUNMI_APP_KEY="your_app_key" +``` + +2. Run any example script: + +```bash +python3 02_get_terminal_list.py +``` + +## Project Structure + +``` +├── README.md # This file +├── requirements.txt # Python dependencies +├── __init__.py # Package init +├── client.py # SunmiAppStoreClient (signing, requests, uploads) +├── 01_get_classify_list.py # Get App Category List +├── 02_get_terminal_list.py # Get Terminal List +├── 03_get_language_list.py # Get Language List +├── 04_upload_apk.py # Upload APK +├── 05_upload_image.py # Upload Image +├── 06_create_app.py # Create App +├── 07_get_app_detail.py # Get App Detail +├── 08_update_app_detail.py # Update App Detail +├── 09_upgrade_app_version.py # Upgrade App Version +├── 10_get_audit_result.py # Get Audit Result +├── 11_get_app_newest_version.py # Get App Newest Version Info +├── 12_update_app_version_detail.py # Update Version Deployment Detail +└── 13_remove_app.py # Remove App +``` + +## Using the Client in Your Code + +```python +import os +from client import SunmiAppStoreClient + +client = SunmiAppStoreClient( + app_id=os.environ["SUNMI_APP_ID"], + app_key=os.environ["SUNMI_APP_KEY"], +) + +# JSON POST request +response = client.post("/v2/appstore/appstore/app/getTerminalList", {}) +print(response) + +# File upload +md5 = SunmiAppStoreClient.calculate_md5("your_app.apk") +response = client.upload( + "/v2/midplat/filecore/file/uploadApk", + "your_app.apk", + {"md5": md5, "file_type_key": "appstore_apk"}, +) +print(response["data"]["uuid"]) +``` + diff --git a/appstore/__init__.py b/appstore/__init__.py new file mode 100644 index 0000000..1c0c7d5 --- /dev/null +++ b/appstore/__init__.py @@ -0,0 +1,3 @@ +from .client import SunmiAppStoreClient + +__all__ = ["SunmiAppStoreClient"] diff --git a/appstore/client.py b/appstore/client.py new file mode 100644 index 0000000..44762a7 --- /dev/null +++ b/appstore/client.py @@ -0,0 +1,141 @@ +""" +Sunmi AppStore OpenAPI Client + +A lightweight Python client for the Sunmi AppStore OpenAPI. +Handles HMAC-SHA256 request signing, JSON POST requests, and multipart file uploads. + +Usage: + from client import SunmiAppStoreClient + + client = SunmiAppStoreClient(app_id="YOUR_APP_ID", app_key="YOUR_APP_KEY") + response = client.post("/v2/appstore/appstore/app/getTerminalList", {}) +""" + +import hashlib +import hmac +import json +import random +import time + +import requests +from requests_toolbelt.multipart.encoder import MultipartEncoder + + +_DEFAULT_BASE_URL = "https://openapi.sunmi.com" +_USER_AGENT = "sunmi-appstore-python-sdk" + + +class SunmiAppStoreClient: + """Client for interacting with the Sunmi AppStore OpenAPI. + + Args: + app_id: Your Sunmi OpenAPI App ID. + app_key: Your Sunmi OpenAPI App Key. + base_url: API base URL. Defaults to the production endpoint. + timeout: Request timeout in seconds. Defaults to 30. + """ + + def __init__(self, app_id: str, app_key: str, base_url: str = _DEFAULT_BASE_URL, timeout: int = 30): + self.app_id = app_id + self.app_key = app_key + self.base_url = base_url.rstrip("/") + self.timeout = timeout + + def _sign(self, json_body: str, timestamp: str, nonce: str) -> str: + """Generate HMAC-SHA256 signature. + + Signature input: json_body + app_id + timestamp + nonce + """ + message = (json_body + self.app_id + timestamp + nonce).encode("utf-8") + return hmac.new(self.app_key.encode("utf-8"), message, hashlib.sha256).hexdigest() + + def _build_headers(self, json_body: str) -> dict: + """Build authentication headers required by the Sunmi OpenAPI.""" + timestamp = str(int(time.time())) + nonce = str(random.randint(100000, 999999)) + sign_str = self._sign(json_body, timestamp, nonce) + return { + "User-Agent": _USER_AGENT, + "Sunmi-Timestamp": timestamp, + "Sunmi-Sign": sign_str, + "Sunmi-Nonce": nonce, + "Sunmi-Appid": self.app_id, + } + + def post(self, path: str, body: dict) -> dict: + """Send a JSON POST request. + + Args: + path: API path, e.g. "/v2/appstore/appstore/app/getTerminalList". + body: Request body as a dictionary. + + Returns: + Parsed JSON response as a dictionary. + + Raises: + requests.HTTPError: If the HTTP response status is not 200. + """ + url = self.base_url + path + json_body = json.dumps(body) + headers = self._build_headers(json_body) + headers["Content-Type"] = "application/json" + + response = requests.post(url, json=body, headers=headers, timeout=self.timeout) + response.raise_for_status() + return response.json() + + def upload(self, path: str, file_path: str, params: dict, check: str = "no") -> dict: + """Upload a file via multipart/form-data POST. + + The signature is computed over the JSON-encoded params string. + + Args: + path: API path, e.g. "/v2/midplat/filecore/file/uploadApk". + file_path: Local path to the file to upload. + params: Upload parameters (e.g. {"md5": "...", "file_type_key": "..."}). + check: Check flag, defaults to "no". + + Returns: + Parsed JSON response as a dictionary. + + Raises: + FileNotFoundError: If the file does not exist. + requests.HTTPError: If the HTTP response status is not 200. + """ + url = self.base_url + path + params_json = json.dumps(params) + headers = self._build_headers(params_json) + + file_name = file_path.rsplit("/", 1)[-1].rsplit("\\", 1)[-1] + with open(file_path, "rb") as f: + multipart_data = MultipartEncoder( + fields={ + "check": check, + "params": params_json, + "file": (file_name, f, "application/octet-stream"), + } + ) + headers["Content-Type"] = multipart_data.content_type + + response = requests.post(url, data=multipart_data, headers=headers, timeout=self.timeout) + + response.raise_for_status() + return response.json() + + @staticmethod + def calculate_md5(file_path: str) -> str: + """Calculate the MD5 hash of a file. + + Reads the file in chunks to support large files efficiently. + + Args: + file_path: Path to the file. + + Returns: + Hex-encoded MD5 digest string. + """ + md5_hash = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() diff --git a/appstore/requirements.txt b/appstore/requirements.txt new file mode 100644 index 0000000..de02f8a --- /dev/null +++ b/appstore/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.20.0 +requests-toolbelt>=0.9.1