├── .editorconfig ├── .gitignore ├── MANIFEST.in ├── Pipfile ├── README.md ├── pywechatpay ├── __init__.py ├── constants.py ├── core │ ├── __init__.py │ ├── cipher.py │ ├── client.py │ ├── credential.py │ ├── downloader.py │ ├── downloader_mgr.py │ ├── notify.py │ ├── signer.py │ ├── validator.py │ └── verifier.py ├── exceptions.py ├── services │ ├── README.md │ ├── __init__.py │ ├── payments │ │ ├── __init__.py │ │ ├── app.py │ │ ├── h5.py │ │ └── jsapi.py │ └── service.py └── utils │ ├── __init__.py │ ├── aes.py │ ├── nonce.py │ ├── pem.py │ ├── rsa.py │ └── sign.py ├── requirements.txt └── setup.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = crlf 11 | charset = utf-8 12 | 13 | # Docstrings and comments use max_line_length = 79 14 | [*.py] 15 | max_line_length = 119 16 | 17 | # Use 2 spaces for the HTML files 18 | [*.html] 19 | indent_size = 2 20 | 21 | # The JSON files contain newlines inconsistently 22 | [*.json] 23 | indent_size = 2 24 | insert_final_newline = ignore 25 | 26 | [**/admin/js/vendor/**] 27 | indent_style = ignore 28 | indent_size = ignore 29 | 30 | # Minified JavaScript files shouldn't be changed 31 | [**.min.js] 32 | indent_style = ignore 33 | insert_final_newline = ignore 34 | 35 | # Makefiles always use tabs for indentation 36 | [Makefile] 37 | indent_style = tab 38 | 39 | # Batch files use tabs for indentation 40 | [*.bat] 41 | indent_style = tab 42 | 43 | [docs/**.txt] 44 | max_line_length = 79 45 | 46 | [*.yml] 47 | indent_size = 2 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | .vscode/ 133 | .idea/ -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | cryptography = "*" 10 | requests = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | 15 | [pipenv] 16 | allow_prereleases = true 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pywechatpay 2 | 3 | [![PyPI version](https://badge.fury.io/py/pywechatpay.svg)](https://badge.fury.io/py/pywechatpay) 4 | 5 | **pywechatpay** 是微信支付`V3`版接口的`python SDK`. 6 | 7 | ## 功能介绍 8 | 9 | 1. 接口 SDK. 请看 `services` 里面的 `README.md` 文档. 10 | 2. HTTP 客户端 `core.client`, 支持请求签名和应答验签. 如果 SDK 未支持你需要的接口, 请用此客户端发起请求. 11 | 3. 回调通知处理 `core.notify`, 支持微信支付回调通知的验证和解密. 12 | 4. 证书下载等辅助能力 13 | 14 | ## 使用教程 15 | 16 | ### 安装 17 | 18 | 从 PyPi 安装: 19 | 20 | ``` 21 | $ pip install pywechatpay 22 | ``` 23 | 24 | ### 准备 25 | 26 | 参考微信官方文档准备好密钥, 证书文件和配置( [证书/密钥/签名介绍](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_0.shtml)) 27 | 28 | ### 初始化 29 | 30 | ``` python 31 | from pywechatpay.core.client import with_wechat_pay_auto_auth_cipher 32 | 33 | 34 | MCH_ID = "xxx" 35 | MCH_SERIAL_NO = "xxx" 36 | MCH_PRIVATE_KEY_STRING = "xxx" 37 | APIv3_KEY = "xxx" 38 | 39 | # 初始化 client, 并使它具有获取微信支付平台证书的能力 40 | client = with_wechat_pay_auto_auth_cipher(MCH_ID, MCH_SERIAL_NO, MCH_PRIVATE_KEY_STRING, APIv3_KEY) 41 | ``` 42 | 43 | ### 接口 44 | 45 | - APP支付 [pay/transactions/app](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml) 46 | 47 | ```python 48 | from pywechatpay.services.payments.app import AppApiService 49 | 50 | svc = AppApiService(client) 51 | 52 | # 方式一, 返回客户端调起微信支付的参数 53 | result = svc.prepay_with_request_payment(appid="xxx", mchid="xxx", description="xxx", out_trade_no="xxx", total=1, 54 | notify_url="xxx") 55 | 56 | # 方式二, 返回原微信返回的响应 57 | result = svc.pay_transactions_app(appid="xxx", mchid="xxx", description="xxx", out_trade_no="xxx", total=1, 58 | notify_url="xxx") 59 | 60 | # 查询订单 61 | # 微信支付订单号查询 62 | result = svc.pay_transactions_id(mchid="xxx", transaction_id="xxx") 63 | # 商户订单号查询 64 | result = svc.pay_transactions_out_trade_no(mchid="xxx", out_trade_no="xxx") 65 | ``` 66 | 67 | - H5支付 [pay/transactions/h5](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml) 68 | 69 | ```python 70 | from pywechatpay.services.payments.h5 import H5ApiService 71 | 72 | svc = H5ApiService(client) 73 | result = svc.pay_transactions_h5(appid="xxx", mchid="xxx", description="xxx", out_trade_no="xxx", total=1, 74 | payer_client_ip="x.x.x.x", notify_url="xxx") 75 | ``` 76 | 77 | ### 发送 HTTP 请求 78 | 79 | 如果 SDK 还未支持你需要的接口, 使用 core.client.Client 的 GET,POST 等方法发送 HTTP 请求,而不用关注签名,验签等逻辑 80 | 81 | ```python 82 | # Native支付 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml 83 | url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native" 84 | content = { 85 | "appid": "xxx", 86 | "mchid": "xxx", 87 | "description": "xxx", 88 | "out_trade_no": "xxx", 89 | "notify_url": "xxx", 90 | "amount": {"total": 1}, 91 | } 92 | response = client.request("post", url, json=content) 93 | print(response.json()) 94 | ``` 95 | 96 | ### 回调通知的验签和解密 97 | 98 | ```python 99 | from pywechatpay.core import downloader_mgr 100 | from pywechatpay.core.notify import new_notify_handler 101 | from pywechatpay.core.verifier import SHA256WithRSAVerifier 102 | 103 | # 为回调请求的头部, 字典类型 104 | headers = { 105 | "xxx": "xxx", 106 | } 107 | # 为回调请求的内容, 字符串类型 108 | body = "xxx" 109 | 110 | cert_visitor = downloader_mgr.mgr_instance.get_certificate_visitor(mch_id="xxx") 111 | handler = new_notify_handler(mch_api_v3_key="xxx", verifier=SHA256WithRSAVerifier(cert_visitor)) 112 | notify_req = handler.parse_notify_request(headers=headers, body=body) 113 | ``` 114 | 115 | ## 参考链接 116 | 117 | - [wechatpay-apiv3/wechatpay-go](https://github.com/wechatpay-apiv3/wechatpay-go) 118 | 119 | -------------------------------------------------------------------------------- /pywechatpay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/__init__.py -------------------------------------------------------------------------------- /pywechatpay/constants.py: -------------------------------------------------------------------------------- 1 | # SDK 相关信息 2 | VERSION = "0.1.0" # SDK 版本 3 | USER_AGENT_FORMAT = "PyWechatPay/%s" # UserAgent中的信息 4 | 5 | # 微信支付 API 地址 6 | WECHAT_PAY_API_SERVER = 'https://api.mch.weixin.qq.com' 7 | 8 | # 请求报文签名相关常量 9 | SIGNATURE_MESSAGE_FORMAT = '%s\n%s\n%d\n%s\n%s\n' # 数字签名原文格式 10 | # 请求头中的 Authorization 拼接格式 11 | HEADER_AUTHORIZATION_FORMAT = '%s mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"' 12 | 13 | # 默认超时时间 14 | DEFAULT_TIMEOUT = 30 15 | -------------------------------------------------------------------------------- /pywechatpay/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/core/__init__.py -------------------------------------------------------------------------------- /pywechatpay/core/cipher.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/core/cipher.py -------------------------------------------------------------------------------- /pywechatpay/core/client.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | from urllib.parse import urlparse 3 | 4 | import requests 5 | 6 | from .credential import WechatPayCredential 7 | from .downloader_mgr import mgr_instance 8 | from .signer import Sha256WithRSASigner, SignatureResult 9 | from .validator import WechatPayResponseValidator 10 | from .verifier import SHA256WithRSAVerifier 11 | from ..constants import VERSION, USER_AGENT_FORMAT, DEFAULT_TIMEOUT 12 | from ..exceptions import WechatPayAPIException 13 | from ..utils.pem import load_private_key 14 | 15 | 16 | class Client: 17 | def __init__(self, signer=None, credential=None, validator=None, cipher=None): 18 | """ 19 | 20 | :param signer: 签名器 21 | :param credential: 认证器 22 | :param validator: 验证器 23 | :param cipher: 加解密器 24 | """ 25 | self.signer = signer 26 | self.credential = credential 27 | self.validator = validator 28 | self.cipher = cipher 29 | 30 | self.http_client = requests.Session() 31 | 32 | def request(self, method, url, params=None, data=None, json=None, headers={}, **kwargs): 33 | body = data or json 34 | sign_body = body if isinstance(body, str) else dumps(body) if body else "" 35 | up = urlparse(url) 36 | query = f"?{up.query}" if up.query else "" 37 | path = up.path + query 38 | authorization = self.credential.gen_authorization_header(method, path, sign_body) 39 | _headers = { 40 | "User-Agent": USER_AGENT_FORMAT % VERSION, 41 | "Authorization": authorization, 42 | } 43 | headers.update(_headers) 44 | 45 | if "timeout" not in kwargs: 46 | kwargs["timeout"] = DEFAULT_TIMEOUT 47 | 48 | response = self.http_client.request(method, url, params, data, headers=headers, json=json, **kwargs) 49 | 50 | # check is success 51 | self.check_response(response) 52 | 53 | # validate signature 54 | self.validator.validate(response.headers, response.text) 55 | 56 | return response 57 | 58 | @staticmethod 59 | def check_response(resp): 60 | if 200 <= resp.status_code <= 299: 61 | return 62 | 63 | raise WechatPayAPIException(resp.text) 64 | 65 | def sign(self, message: str) -> SignatureResult: 66 | """ 67 | 使用 signer 对字符串进行签名 68 | 69 | :param message: 待签名字符串 70 | :return: 71 | """ 72 | return self.signer.sign(message) 73 | 74 | 75 | def with_wechat_pay_auto_auth_cipher_using_downloader_mgr(mch_id: str, mch_cert_serial_no: str, mch_private_key: str, 76 | mgr) -> Client: 77 | """一键初始化 Client,使其具备「签名/验签/敏感字段加解密」能力。 78 | 需要使用者自行提供 CertificateDownloaderMgr 实现平台证书的自动更新 79 | """ 80 | cert_visitor = mgr.get_certificate_visitor(mch_id) 81 | 82 | private_key = load_private_key(mch_private_key) 83 | signer = Sha256WithRSASigner(mch_id, mch_cert_serial_no, private_key) 84 | credential = WechatPayCredential(signer) 85 | validator = WechatPayResponseValidator(SHA256WithRSAVerifier(cert_visitor)) 86 | return Client(signer=signer, credential=credential, validator=validator) 87 | 88 | 89 | def with_wechat_pay_auto_auth_cipher(mch_id: str, mch_cert_serial_no: str, mch_private_key: str, 90 | mch_api_v3_key: str) -> Client: 91 | """ 92 | 一键初始化 Client,使其具备「签名/验签/敏感字段加解密」能力。 93 | 同时提供证书定时更新功能(因此需要提供 mchAPIv3Key 用于证书解密),不再需要本地提供平台证书 94 | 95 | :param mch_id: 商户号 96 | :param mch_cert_serial_no: 商户证书序列号 97 | :param mch_private_key: 商户证书私钥 98 | :param mch_api_v3_key: 商户APIv3密钥 99 | :return: 100 | """ 101 | if not mgr_instance.has_downloader(mch_id): 102 | mgr_instance.register_downloader_with_private_key(mch_id=mch_id, mch_cert_serial_no=mch_cert_serial_no, 103 | mch_private_key=mch_private_key, 104 | mch_api_v3_key=mch_api_v3_key) 105 | return with_wechat_pay_auto_auth_cipher_using_downloader_mgr(mch_id, mch_cert_serial_no, mch_private_key, 106 | mgr_instance) 107 | -------------------------------------------------------------------------------- /pywechatpay/core/credential.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .signer import Signer 4 | from ..constants import SIGNATURE_MESSAGE_FORMAT, HEADER_AUTHORIZATION_FORMAT 5 | from ..utils.nonce import gen_noncestr 6 | 7 | 8 | class WechatPayCredential: 9 | """认证器""" 10 | 11 | def __init__(self, signer: Signer): 12 | """ 13 | 14 | :param signer: 签名器 15 | """ 16 | self.signer = signer 17 | 18 | def gen_authorization_header(self, method: str, url: str, body: str) -> str: 19 | """ 20 | 签名生成 21 | https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml 22 | 23 | :param method: 请求方法 24 | :param url: 请求的绝对URL,并去除域名部分得到参与签名的URL 25 | :param body: 请求报文主体 26 | :return: 27 | """ 28 | timestamp = time.time() 29 | nonce_str = gen_noncestr() 30 | message = SIGNATURE_MESSAGE_FORMAT % (method.upper(), url, timestamp, nonce_str, body) 31 | signature_result = self.signer.sign(message) 32 | authorization_type = f"WECHATPAY2-{self.signer.algorithm()}" 33 | authorization = HEADER_AUTHORIZATION_FORMAT % (authorization_type, signature_result.mch_id, nonce_str, 34 | timestamp, signature_result.cert_serial_no, 35 | signature_result.signature) 36 | return authorization 37 | -------------------------------------------------------------------------------- /pywechatpay/core/downloader.py: -------------------------------------------------------------------------------- 1 | from .credential import WechatPayCredential 2 | from .signer import Sha256WithRSASigner 3 | from .validator import WechatPayResponseValidator, NullValidateor 4 | from .verifier import SHA256WithRSAVerifier 5 | from ..constants import WECHAT_PAY_API_SERVER 6 | from ..exceptions import WechatPayException 7 | from ..utils.aes import decrypt_aes246gcm 8 | from ..utils.pem import load_certificate, load_private_key 9 | 10 | 11 | class CertificateDownloader: 12 | """证书下载器""" 13 | 14 | def __init__(self, client, mch_api_v3_key: str): 15 | self.client = client 16 | self.mch_api_v3_key = mch_api_v3_key 17 | 18 | self.cert_contents = {} 19 | self.certificates = {} 20 | 21 | def get(self, serial_no: str): 22 | """ 23 | 获取证书序列号对应的平台证书 24 | 25 | :param serial_no: 证书序列号 26 | :return: 27 | """ 28 | return self.certificates.get(serial_no) 29 | 30 | def get_newest_serial(self): 31 | """获取最新的平台证书的证书序列号""" 32 | return "" 33 | 34 | def download_certificates(self): 35 | """立即下载平台证书列表""" 36 | url = WECHAT_PAY_API_SERVER + "/v3/certificates" 37 | result = self.client.request("get", url) 38 | data = result.json() 39 | raw_cert_content_map = {} 40 | certificate_map = {} 41 | for encrypt_certificate in data["data"]: 42 | cert_content = self.decrypt_certificate(encrypt_certificate["encrypt_certificate"]) 43 | certificate = load_certificate(cert_content) 44 | 45 | serial_no = encrypt_certificate["serial_no"] 46 | raw_cert_content_map[serial_no] = cert_content 47 | certificate_map[serial_no] = certificate 48 | 49 | if len(certificate_map.keys()) == 0: 50 | raise WechatPayException("no certificate downloaded") 51 | 52 | self.update_certificates(raw_cert_content_map, certificate_map) 53 | 54 | def decrypt_certificate(self, encrypt_certificate): 55 | try: 56 | cert_content = decrypt_aes246gcm(self.mch_api_v3_key, encrypt_certificate["nonce"], 57 | encrypt_certificate["ciphertext"], encrypt_certificate["associated_data"]) 58 | except Exception as ex: 59 | raise WechatPayException(f"decrypt downloaded certificate failed:{ex}") 60 | return cert_content 61 | 62 | def update_certificates(self, cert_contents, certificates): 63 | self.cert_contents = cert_contents 64 | self.certificates = certificates 65 | 66 | self.client.validator = WechatPayResponseValidator(SHA256WithRSAVerifier(certificates)) 67 | 68 | 69 | def new_certificate_downloader_with_client(client, mch_api_v3_key: str) -> CertificateDownloader: 70 | downloader = CertificateDownloader(client=client, mch_api_v3_key=mch_api_v3_key) 71 | downloader.download_certificates() 72 | return downloader 73 | 74 | 75 | def new_certificate_downloader(mch_id: str, mch_cert_serial_no: str, mch_private_key: str, 76 | mch_api_v3_key: str) -> CertificateDownloader: 77 | """ 78 | 创建证书下载器 79 | 80 | :param mch_id: 81 | :param mch_cert_serial_no: 82 | :param mch_private_key: 83 | :param mch_api_v3_key: 84 | :return: 85 | """ 86 | private_key = load_private_key(mch_private_key) 87 | signer = Sha256WithRSASigner(mch_id, mch_cert_serial_no, private_key) 88 | credential = WechatPayCredential(signer) 89 | validator = NullValidateor() 90 | 91 | from .client import Client 92 | client = Client(signer=signer, credential=credential, validator=validator) 93 | return new_certificate_downloader_with_client(client=client, mch_api_v3_key=mch_api_v3_key) 94 | -------------------------------------------------------------------------------- /pywechatpay/core/downloader_mgr.py: -------------------------------------------------------------------------------- 1 | from .downloader import new_certificate_downloader 2 | 3 | 4 | class PseudoCertificateDownloader: 5 | def __init__(self, mgr, mch_id: str): 6 | self.mgr = mgr 7 | self.mch_id = mch_id 8 | 9 | def get(self, serial_no: str): 10 | """ 11 | 获取某序列号的平台证书 12 | 13 | :param serial_no: 证书序列号 14 | :return: 15 | """ 16 | return self.mgr.get_certificate(self.mch_id, serial_no) 17 | 18 | 19 | class CertificateDownloaderMgr: 20 | """证书下载管理器""" 21 | 22 | def __init__(self): 23 | self.downloader_map = {} 24 | 25 | def get_certificate(self, mch_id: str, serial_no: str): 26 | """ 27 | 获取商户的某个平台证书 28 | 29 | :param mch_id: 商户号 30 | :param serial_no: 证书序列号 31 | :return: 32 | """ 33 | downloader = self.downloader_map[mch_id] 34 | return downloader.get(serial_no) 35 | 36 | def get_certificate_visitor(self, mch_id: str): 37 | """ 38 | 获取某个商户的平台证书访问器 39 | 40 | :param mch_id: 商户号 41 | :return: 42 | """ 43 | return PseudoCertificateDownloader(self, mch_id) 44 | 45 | def has_downloader(self, mch_id: str) -> bool: 46 | """ 47 | 检查是否已经注册过 mchID 这个商户的下载器 48 | 49 | :param mch_id: 商户号 50 | :return: 51 | """ 52 | return mch_id in self.downloader_map 53 | 54 | def register_downloader_with_private_key(self, mch_id: str, mch_cert_serial_no: str, mch_private_key: str, 55 | mch_api_v3_key: str): 56 | """ 57 | 注册商户的平台证书下载器 58 | 59 | :param mch_id: 60 | :param mch_cert_serial_no: 61 | :param mch_private_key: 62 | :param mch_api_v3_key: 63 | :return: 64 | """ 65 | downloader = new_certificate_downloader(mch_id, mch_cert_serial_no, mch_private_key, mch_api_v3_key) 66 | self.downloader_map[mch_id] = downloader 67 | 68 | 69 | # 下载管理器单例 70 | mgr_instance = CertificateDownloaderMgr() 71 | -------------------------------------------------------------------------------- /pywechatpay/core/notify.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .validator import WechatPayNotifyValidator, Validator 4 | from .verifier import Verifier 5 | from ..exceptions import WechatPayException 6 | from ..utils.aes import decrypt_aes246gcm 7 | 8 | 9 | class Handler: 10 | """微信支付通知 Handler""" 11 | 12 | def __init__(self, mch_api_v3_key: str, validator: Validator): 13 | """ 14 | 15 | :param mch_api_v3_key: 商户APIv3密钥 16 | :param validator: 验证器 17 | """ 18 | self.mch_api_v3_key = mch_api_v3_key 19 | self.validator = validator 20 | 21 | def parse_notify_request(self, headers: dict, body: str): 22 | """ 23 | 解析微信支付通知 24 | 25 | :param headers: 请求头 26 | :param body: 请求主体 27 | :return: 28 | """ 29 | try: 30 | self.validator.validate(headers=headers, body=body) 31 | except Exception as ex: 32 | raise WechatPayException(f"not valid pywechatpay notify:{ex}") 33 | 34 | ret = json.loads(body) 35 | plain_text = decrypt_aes246gcm(self.mch_api_v3_key, ret["resource"]["nonce"], ret["resource"]["ciphertext"], 36 | ret["resource"]["associated_data"]) 37 | return plain_text 38 | 39 | 40 | def new_notify_handler(mch_api_v3_key: str, verifier: Verifier) -> Handler: 41 | """ 42 | 创建通知处理器 43 | 44 | :param mch_api_v3_key: 商户APIv3密钥 45 | :param verifier: 验证者 46 | :return: 47 | """ 48 | return Handler(mch_api_v3_key=mch_api_v3_key, validator=WechatPayNotifyValidator(verifier)) 49 | -------------------------------------------------------------------------------- /pywechatpay/core/signer.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from collections import namedtuple 3 | 4 | from ..utils.sign import sign_sha256_with_rsa 5 | 6 | SignatureResult = namedtuple("SignatureResult", ["mch_id", "cert_serial_no", "signature"]) 7 | 8 | 9 | class Signer(metaclass=abc.ABCMeta): 10 | @abc.abstractmethod 11 | def sign(self, message: str) -> SignatureResult: 12 | """ 13 | 签名 14 | 15 | :param message: 待签名字符串 16 | :return: 17 | """ 18 | 19 | @abc.abstractmethod 20 | def algorithm(self) -> str: 21 | """签名算法名称""" 22 | 23 | 24 | class Sha256WithRSASigner(Signer): 25 | def __init__(self, mch_id: str, cert_serial_no: str, private_key): 26 | self.mch_id = mch_id 27 | self.cert_serial_no = cert_serial_no 28 | self.private_key = private_key 29 | 30 | def sign(self, message: str) -> SignatureResult: 31 | signature = sign_sha256_with_rsa(message, self.private_key) 32 | return SignatureResult(self.mch_id, self.cert_serial_no, signature) 33 | 34 | def algorithm(self) -> str: 35 | return "SHA256-RSA2048" 36 | -------------------------------------------------------------------------------- /pywechatpay/core/validator.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from .verifier import Verifier 4 | from ..exceptions import WechatPayException 5 | 6 | 7 | class Validator(metaclass=abc.ABCMeta): 8 | """验证器""" 9 | 10 | @abc.abstractmethod 11 | def validate(self, headers: dict, body: str): 12 | """ 13 | 验证. 不通过则报错 14 | 15 | :param headers: 请求头 16 | :param body: 请求主体 17 | :return: 18 | """ 19 | 20 | 21 | class NullValidateor(Validator): 22 | """空验证器,不对报文进行验证,对任意报文均不会返回错误, 23 | 在不需要对报文签名进行验证的情况(如首次证书下载,微信支付账单文件下载)下使用 24 | """ 25 | 26 | def validate(self, headers, body): 27 | return 28 | 29 | 30 | class WechatPayValidator(Validator): 31 | def __init__(self, verifier: Verifier): 32 | self.verifier = verifier 33 | 34 | def validate(self, headers, body): 35 | request_id = headers.get("Request-ID", "") 36 | timestamp = headers.get("Wechatpay-Timestamp", "") 37 | nonce = headers.get("Wechatpay-Nonce", "") 38 | signature = headers.get("Wechatpay-Signature", "") 39 | serial = headers.get("Wechatpay-Serial", "") 40 | 41 | message = "%s\n%s\n%s\n" % (timestamp, nonce, body) 42 | 43 | try: 44 | self.verifier.verify(serial, message, signature) 45 | except Exception as ex: 46 | raise WechatPayException(f"validate verify fail serial=[{serial}] request-id=[{request_id}] err={ex}") 47 | 48 | 49 | class WechatPayResponseValidator(WechatPayValidator): 50 | """微信支付 API v3 默认应答报文验证器""" 51 | pass 52 | 53 | 54 | class WechatPayNotifyValidator(WechatPayValidator): 55 | """对接收到的微信支付 API v3 通知请求报文进行验证""" 56 | pass 57 | -------------------------------------------------------------------------------- /pywechatpay/core/verifier.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from base64 import b64decode 3 | 4 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 5 | from cryptography.hazmat.primitives.hashes import SHA256 6 | 7 | from pywechatpay.exceptions import WechatPayException 8 | 9 | 10 | class Verifier(metaclass=abc.ABCMeta): 11 | """数字签名验证者""" 12 | 13 | @abc.abstractmethod 14 | def verify(self, serial_no: str, message: str, signature: str): 15 | """ 16 | 验证. 不通过则报错 17 | 18 | :param serial_no: 序列号 19 | :param message: 待验签字符串 20 | :param signature: 签名 21 | :return: 22 | """ 23 | 24 | 25 | class SHA256WithRSAVerifier(Verifier): 26 | """SHA256WithRSA 数字签名验证者""" 27 | 28 | def __init__(self, cert_getter): 29 | self.cert_getter = cert_getter 30 | 31 | def verify(self, serial_no, message, signature): 32 | message_bytes = str.encode(message) 33 | signature = b64decode(signature) 34 | certificate = self.cert_getter.get(serial_no) 35 | if not certificate: 36 | raise WechatPayException(f"certificate[{serial_no}] not found in verifier") 37 | 38 | public_key = certificate.public_key() 39 | 40 | try: 41 | public_key.verify(signature, message_bytes, PKCS1v15(), SHA256()) 42 | except Exception as ex: 43 | raise WechatPayException(f"validate verify fail serial=[{serial_no}] err={ex}") 44 | -------------------------------------------------------------------------------- /pywechatpay/exceptions.py: -------------------------------------------------------------------------------- 1 | class WechatPayException(Exception): 2 | pass 3 | 4 | 5 | class WechatPayAPIException(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /pywechatpay/services/README.md: -------------------------------------------------------------------------------- 1 | # 业务接口介绍 2 | 3 | --- 4 | 5 | 没有实现的接口可以提交合并请求. 6 | 7 | | 目录 | 业务|直连商户|服务商| 8 | | --- | --- | --- | --- | 9 | | certificates | 平台证书 | ️ | ️ | 10 | | payments/app | APP 支付| ✔️ | | 11 | | payments/jsapi | JSAPI 支付| ✔️ | | 12 | | payments/native | Native 支付 | ️ | | 13 | | payments/h5 | H5 支付| ✔️ | | 14 | | partnerpayments/app | APP 支付| | ️ | 15 | | partnerpayments/jsapi | JSAPI 支付| | ️| 16 | | partnerpayments/native | Native 支付 ||️| 17 | | partnerpayments/h5 | H5 支付||️| 18 | | profitsharing | 分账|️|️| 19 | | refunddomestic | 退款|️|️| 20 | | transferbatch|批量转账到零钱|| | 21 | | partnertransferbatch|批量转账到零钱| || 22 | -------------------------------------------------------------------------------- /pywechatpay/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/services/__init__.py -------------------------------------------------------------------------------- /pywechatpay/services/payments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/services/payments/__init__.py -------------------------------------------------------------------------------- /pywechatpay/services/payments/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from ..service import ServiceABC 4 | from ...constants import WECHAT_PAY_API_SERVER 5 | from ...utils.nonce import gen_noncestr 6 | 7 | 8 | class AppApiService(ServiceABC): 9 | def pay_transactions_app(self, appid: str, mchid: str, description: str, out_trade_no: str, total: int, 10 | notify_url: str, currency: str = "CNY", **kwargs) -> dict: 11 | """ 12 | APP下单API 13 | https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml 14 | 15 | :param appid: 应用ID 16 | :param mchid: 直连商户号 17 | :param description: 商品描述 18 | :param out_trade_no: 商户订单号 19 | :param total: 订单总金额,单位为分 20 | :param notify_url: 通知地址, 通知URL必须为直接可访问的URL,不允许携带查询串 21 | :param currency: 货币类型, CNY:人民币 22 | :param kwargs: 可选参数 23 | :return: 24 | """ 25 | content = { 26 | "appid": appid, 27 | "mchid": mchid, 28 | "description": description, 29 | "out_trade_no": out_trade_no, 30 | "notify_url": notify_url, 31 | "amount": {"total": total, "currency": currency}, 32 | } 33 | content.update(kwargs) 34 | url = WECHAT_PAY_API_SERVER + "/v3/pay/transactions/app" 35 | result = self.client.request("post", url, json=content) 36 | return result.json() 37 | 38 | def prepay_with_request_payment(self, appid: str, mchid: str, description: str, out_trade_no: str, total: int, 39 | notify_url: str, currency: str = "CNY", **kwargs) -> dict: 40 | """ 41 | APP支付下单,并返回调起支付的请求参数 42 | https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml 43 | 44 | :param appid: 应用ID 45 | :param mchid: 直连商户号 46 | :param description: 商品描述 47 | :param out_trade_no: 商户订单号 48 | :param total: 订单总金额,单位为分 49 | :param notify_url: 通知地址, 通知URL必须为直接可访问的URL,不允许携带查询串 50 | :param currency: 货币类型, CNY:人民币 51 | :param kwargs: 可选参数 52 | :return: 53 | """ 54 | result = self.pay_transactions_app(appid, mchid, description, out_trade_no, total, notify_url, currency, 55 | **kwargs) 56 | prepay_id = result["prepay_id"] 57 | timestamp = str(int(time.time())) 58 | nonce_str = gen_noncestr() 59 | message = "%s\n%s\n%s\n%s\n" % (appid, timestamp, nonce_str, prepay_id) 60 | pay_sign = self.client.sign(message).signature 61 | request_payment = { 62 | "appid": appid, 63 | "partnerid": mchid, 64 | "prepayid": prepay_id, 65 | "package": "Sign=WXPay", 66 | "noncestr": nonce_str, 67 | "timestamp": timestamp, 68 | "sign": pay_sign, 69 | } 70 | return request_payment 71 | 72 | def pay_transactions_id(self, mchid: str, transaction_id: str) -> dict: 73 | """ 74 | 微信支付订单号查询 75 | 76 | :param mchid: 直连商户号 77 | :param transaction_id: 微信支付订单号 78 | :return: 79 | """ 80 | url = WECHAT_PAY_API_SERVER + f"/v3/pay/transactions/id/{transaction_id}?mchid={mchid}" 81 | result = self.client.request("get", url) 82 | return result.json() 83 | 84 | def pay_transactions_out_trade_no(self, mchid: str, out_trade_no: str) -> dict: 85 | """ 86 | 商户订单号查询 87 | 88 | :param mchid: 直连商户号 89 | :param out_trade_no: 商户订单号 90 | :return: 91 | """ 92 | url = WECHAT_PAY_API_SERVER + f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={mchid}" 93 | result = self.client.request("get", url) 94 | return result.json() 95 | -------------------------------------------------------------------------------- /pywechatpay/services/payments/h5.py: -------------------------------------------------------------------------------- 1 | from ..service import ServiceABC 2 | from ...constants import WECHAT_PAY_API_SERVER 3 | 4 | 5 | class H5ApiService(ServiceABC): 6 | def pay_transactions_h5(self, appid: str, mchid: str, description: str, out_trade_no: str, total: int, 7 | notify_url: str, payer_client_ip: str, type: str = "Wap", currency: str = "CNY", 8 | **kwargs) -> dict: 9 | """ 10 | H5下单API 11 | https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml 12 | 13 | :param appid: 应用ID 14 | :param mchid: 直连商户号 15 | :param description: 商品描述 16 | :param out_trade_no: 商户订单号 17 | :param total: 订单总金额,单位为分 18 | :param notify_url: 通知地址, 通知URL必须为直接可访问的URL,不允许携带查询串 19 | :param payer_client_ip: 订单总金额,单位为分 20 | :param type: 场景类型, 示例值:iOS, Android, Wap 21 | :param currency: 货币类型, CNY:人民币 22 | :param kwargs: 可选参数 23 | :return: 24 | """ 25 | content = { 26 | "appid": appid, 27 | "mchid": mchid, 28 | "description": description, 29 | "out_trade_no": out_trade_no, 30 | "notify_url": notify_url, 31 | "amount": {"total": total, "currency": currency}, 32 | "scene_info": { 33 | "payer_client_ip": payer_client_ip, 34 | "h5_info": {"type": type}, 35 | }, 36 | } 37 | content.update(kwargs) 38 | url = WECHAT_PAY_API_SERVER + "/v3/pay/transactions/h5" 39 | result = self.client.request("post", url, json=content) 40 | return result.json() 41 | 42 | def pay_transactions_id(self, mchid: str, transaction_id: str) -> dict: 43 | """ 44 | 微信支付订单号查询 45 | 46 | :param mchid: 直连商户号 47 | :param transaction_id: 微信支付订单号 48 | :return: 49 | """ 50 | url = WECHAT_PAY_API_SERVER + f"/v3/pay/transactions/id/{transaction_id}?mchid={mchid}" 51 | result = self.client.request("get", url) 52 | return result.json() 53 | 54 | def pay_transactions_out_trade_no(self, mchid: str, out_trade_no: str) -> dict: 55 | """ 56 | 商户订单号查询 57 | 58 | :param mchid: 直连商户号 59 | :param out_trade_no: 商户订单号 60 | :return: 61 | """ 62 | url = WECHAT_PAY_API_SERVER + f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={mchid}" 63 | result = self.client.request("get", url) 64 | return result.json() 65 | -------------------------------------------------------------------------------- /pywechatpay/services/payments/jsapi.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from ..service import ServiceABC 4 | from ...constants import WECHAT_PAY_API_SERVER 5 | from ...utils.nonce import gen_noncestr 6 | 7 | 8 | class JsapiApiService(ServiceABC): 9 | def pay_transactions_jsapi(self, appid: str, mchid: str, description: str, out_trade_no: str, total: int, 10 | notify_url: str, openid: str, currency: str = "CNY", **kwargs) -> dict: 11 | """ 12 | JSAPI下单 13 | https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml 14 | 15 | :param appid: 应用ID 16 | :param mchid: 直连商户号 17 | :param description: 商品描述 18 | :param out_trade_no: 商户订单号 19 | :param total: 订单总金额,单位为分 20 | :param notify_url: 通知地址, 通知URL必须为直接可访问的URL,不允许携带查询串 21 | :param openid: 用户在直连商户appid下的唯一标识 22 | :param currency: 货币类型, CNY:人民币 23 | :param kwargs: 可选参数 24 | :return: 25 | """ 26 | content = { 27 | "appid": appid, 28 | "mchid": mchid, 29 | "description": description, 30 | "out_trade_no": out_trade_no, 31 | "notify_url": notify_url, 32 | "amount": {"total": total, "currency": currency}, 33 | "payer": {"openid": openid} 34 | } 35 | content.update(kwargs) 36 | url = WECHAT_PAY_API_SERVER + "/v3/pay/transactions/h5" 37 | result = self.client.request("post", url, json=content) 38 | return result.json() 39 | 40 | def prepay_with_request_payment(self, appid: str, mchid: str, description: str, out_trade_no: str, total: int, 41 | notify_url: str, openid: str, currency: str = "CNY", **kwargs) -> dict: 42 | """ 43 | Jsapi支付下单,并返回调起支付的请求参数 44 | https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml 45 | 46 | :param appid: 应用ID 47 | :param mchid: 直连商户号 48 | :param description: 商品描述 49 | :param out_trade_no: 商户订单号 50 | :param total: 订单总金额,单位为分 51 | :param notify_url: 通知地址, 通知URL必须为直接可访问的URL,不允许携带查询串 52 | :param openid: 用户在直连商户appid下的唯一标识 53 | :param currency: 货币类型, CNY:人民币 54 | :param kwargs: 可选参数 55 | :return: 56 | """ 57 | result = self.pay_transactions_jsapi(appid, mchid, description, out_trade_no, total, notify_url, openid, 58 | currency, **kwargs) 59 | prepay_id = result["prepay_id"] 60 | timestamp = str(int(time.time())) 61 | nonce_str = gen_noncestr() 62 | package = f"prepay_id={prepay_id}" 63 | message = "%s\n%s\n%s\n%s\n" % (appid, timestamp, nonce_str, package) 64 | pay_sign = self.client.sign(message).signature 65 | request_payment = { 66 | "appId": appid, 67 | "timeStamp": timestamp, 68 | "nonceStr": nonce_str, 69 | "package": package, 70 | "signType": "RSA", 71 | "paySign": pay_sign, 72 | } 73 | return request_payment 74 | 75 | def pay_transactions_id(self, mchid: str, transaction_id: str) -> dict: 76 | """ 77 | 微信支付订单号查询 78 | 79 | :param mchid: 直连商户号 80 | :param transaction_id: 微信支付订单号 81 | :return: 82 | """ 83 | url = WECHAT_PAY_API_SERVER + f"/v3/pay/transactions/id/{transaction_id}?mchid={mchid}" 84 | result = self.client.request("get", url) 85 | return result.json() 86 | 87 | def pay_transactions_out_trade_no(self, mchid: str, out_trade_no: str) -> dict: 88 | """ 89 | 商户订单号查询 90 | 91 | :param mchid: 直连商户号 92 | :param out_trade_no: 商户订单号 93 | :return: 94 | """ 95 | url = WECHAT_PAY_API_SERVER + f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={mchid}" 96 | result = self.client.request("get", url) 97 | return result.json() 98 | -------------------------------------------------------------------------------- /pywechatpay/services/service.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | 3 | from ..core.client import Client 4 | 5 | 6 | class ServiceABC(metaclass=ABCMeta): 7 | def __init__(self, client: Client): 8 | self.client = client 9 | -------------------------------------------------------------------------------- /pywechatpay/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/utils/__init__.py -------------------------------------------------------------------------------- /pywechatpay/utils/aes.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | 3 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM 4 | 5 | 6 | def decrypt_aes246gcm(key: str, nonce: str, ciphertext: str, associated_data: str) -> str: 7 | key_bytes = str.encode(key) 8 | nonce_bytes = str.encode(nonce) 9 | ad_bytes = str.encode(associated_data) 10 | data = b64decode(ciphertext) 11 | aesgcm = AESGCM(key_bytes) 12 | return aesgcm.decrypt(nonce_bytes, data, ad_bytes).decode() 13 | -------------------------------------------------------------------------------- /pywechatpay/utils/nonce.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def gen_noncestr(k: int = 32) -> str: 6 | """ 7 | 生成随机串,随机串包含字母或数字 8 | 9 | :param k: 长度 10 | :return: 11 | """ 12 | return "".join(random.sample(string.ascii_letters + string.digits, k)) 13 | -------------------------------------------------------------------------------- /pywechatpay/utils/pem.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives.serialization import load_pem_private_key 2 | from cryptography.x509 import load_pem_x509_certificate 3 | 4 | 5 | def format_private_key(private_key_str): 6 | """ 7 | 规整私钥格式 8 | 9 | :param private_key_str: 私钥字符串 10 | :return: 11 | """ 12 | pem_start = "-----BEGIN PRIVATE KEY-----\n" 13 | pem_end = "\n-----END PRIVATE KEY-----" 14 | if not private_key_str.startswith(pem_start): 15 | private_key_str = pem_start + private_key_str 16 | if not private_key_str.endswith(pem_end): 17 | private_key_str = private_key_str + pem_end 18 | return private_key_str 19 | 20 | 21 | def load_certificate(certificate_str: str): 22 | """ 23 | 载入证书 24 | 25 | :param certificate_str: 证书字符串 26 | :return: 27 | """ 28 | certificate_bytes = str.encode(certificate_str) 29 | return load_pem_x509_certificate(certificate_bytes) 30 | 31 | 32 | def load_private_key(private_key_str: str): 33 | """ 34 | 载入私钥 35 | 36 | :param private_key_str: 私钥字符串 37 | :return: 38 | """ 39 | private_key_bytes = str.encode(format_private_key(private_key_str)) 40 | return load_pem_private_key(private_key_bytes, password=None) 41 | -------------------------------------------------------------------------------- /pywechatpay/utils/rsa.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dust8/pywechatpay/8a18b97c1106b2014a1612be87b0fadcf9080e8e/pywechatpay/utils/rsa.py -------------------------------------------------------------------------------- /pywechatpay/utils/sign.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | 3 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 4 | from cryptography.hazmat.primitives.hashes import SHA256 5 | 6 | 7 | def sign_sha256_with_rsa(message: str, private_key) -> str: 8 | message_bytes = str.encode(message) 9 | signature = private_key.sign(message_bytes, padding=PKCS1v15(), algorithm=SHA256()) 10 | return b64encode(signature).decode() 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography==35.0.0 2 | requests==2.26.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | from pywechatpay.constants import VERSION 4 | 5 | with open("README.md", "r", encoding="utf8") as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name="pywechatpay", 10 | version=VERSION, 11 | author="dust8", 12 | description="Python SDK for WechatPay V3", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | license="BSD", 16 | keywords="python sdk wechatpay v3", 17 | url="https://github.com/dust8/pywechatpay", 18 | packages=["pywechatpay"], 19 | classifiers=[ 20 | "Development Status :: 3 - Alpha", 21 | "Topic :: Utilities", 22 | "License :: OSI Approved :: BSD License", 23 | "Programming Language :: Python :: 3.6", 24 | "Programming Language :: Python :: 3.7", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | ], 29 | install_requires=["cryptography", "requests"], 30 | ) 31 | --------------------------------------------------------------------------------