Initial commit: 首次建仓,建立目录结构
This commit is contained in:
23
README.md
Normal file
23
README.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Beast Trader
|
||||||
|
|
||||||
|
> ETH/USDT 永续合约 — 中低频价格行为策略研发
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
| 目录 | 说明 |
|
||||||
|
|:---|:---|
|
||||||
|
| `strategies/` | 策略代码(按版本归档) |
|
||||||
|
| `backtests/` | 回测结果 |
|
||||||
|
| `trades/` | 实盘交易记录(自动生成) |
|
||||||
|
| `daily_briefs/` | 每日简报(自动生成) |
|
||||||
|
| `docs/` | 文档(手册、报告) |
|
||||||
|
| `config/` | 配置文件 |
|
||||||
|
| `dashboard/` | Beast Trader Dashboard |
|
||||||
|
| `scripts/` | 自动化脚本 |
|
||||||
|
| `tools/` | 分析工具 |
|
||||||
|
|
||||||
|
## 工作流
|
||||||
|
|
||||||
|
编写 → 回测 → 评估 → 发布(tag) → 实盘 → 复盘
|
||||||
|
|
||||||
|
详见 `docs/guides/workflow_reference.md`
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"5d9dbcad66211a164ff6ea751da905abf504163e","backtest_start_time":1781075231,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"0668e50d1dd7394da760d48c358fb89d9940b1a1","backtest_start_time":1781075580,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"dfb6675634b8f42b210664b4c725945953d69c38","backtest_start_time":1781075697,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"7e1a878d1791bcd1831a1606377d8f7f1f9c0b11","backtest_start_time":1781076774,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"7c8a6f0cafe3a24cd12498907194a9f13dfc9f0a","backtest_start_time":1781077325,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"611c4e0a697035bd2fe70b7fb5de30c17f4348ce","backtest_start_time":1781078709,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"ec43a348e14d2e8a0fdd7185d44a06c81c268a20","backtest_start_time":1781078822,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"40f3de7a7034b27ed88379d85c9004a6bdeda26a","backtest_start_time":1781078915,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"5006df21d4680b5858c830464277152b62693f2d","backtest_start_time":1781079411,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1778803200,"backtest_end_ts":1781049600}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowScalp":{"run_id":"8c9173b91d5557afcc9b0a2f22579422a79d5433","backtest_start_time":1781079721,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1704067200}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowMomentumScalp":{"run_id":"55e90e69bb662d449103b9620f51e93c6346fe80","backtest_start_time":1781080191,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1704067200}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowMomentumScalp":{"run_id":"961e38d12b56872445e5f4f32b6a9db3cf560e6f","backtest_start_time":1781080317,"timeframe":"5m","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1704067200}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV30":{"run_id":"3f57b6fb50a476c00713b62bd67ab20bd90c4910","backtest_start_time":1781095529,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1703980800}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV31":{"run_id":"014572729a6218a457183a82370acffdd8e774c5","backtest_start_time":1781096292,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1703980800}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV31":{"run_id":"7dc34d1e93588cd38be7826605224908c275ae4f","backtest_start_time":1781097736,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1735689600,"backtest_end_ts":1767139200}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV31":{"run_id":"294bccacc1613b0ef19c3bb6e9542436c02d857d","backtest_start_time":1781097759,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1751328000,"backtest_end_ts":1756598400}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV32":{"run_id":"f84aa424a0c90ffb223bb6fc5e38f9a10f67db9a","backtest_start_time":1781099073,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1703980800}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV32":{"run_id":"4ec9b78473c02a5ceb38b37d87c539bf2312b18c","backtest_start_time":1781099114,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1703980800}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV32":{"run_id":"c666105d00ba25635608f1fd7d581009b616535a","backtest_start_time":1781099393,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1735689600,"backtest_end_ts":1767139200}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowSwingV32":{"run_id":"d2626584bb8172a85399c4dcb0972772a9f0621f","backtest_start_time":1781099393,"timeframe":"4h","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1703980800}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowStrategyV22c":{"run_id":"09c8c3017d2626514facb9f3d20d0b6601b8f00d","backtest_start_time":1781112343,"timeframe":"1h","timeframe_detail":null,"backtest_start_ts":1672531200,"backtest_end_ts":1767139200}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowStrategyV22d":{"run_id":"dadac4a0466e755cc789643df6326a91a1d44622","backtest_start_time":1781159624,"timeframe":"1h","timeframe_detail":null,"backtest_start_ts":1780963200,"backtest_end_ts":1781136000}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowStrategyV22d":{"run_id":"a486f604bb6528d007ee6459e8b6738e66aa582e","backtest_start_time":1781159650,"timeframe":"1h","timeframe_detail":null,"backtest_start_ts":1780963200,"backtest_end_ts":1781136000}}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{"StructureFlowStrategyV22d":{"run_id":"70aaa0ea25ba897e4b392d156ccfff724d968d5e","backtest_start_time":1781159710,"timeframe":"1h","timeframe_detail":null,"backtest_start_ts":1780963200,"backtest_end_ts":1781136000}}
|
||||||
17
config/docker/docker-compose.yml
Normal file
17
config/docker/docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
freqtrade:
|
||||||
|
image: freqtradeorg/freqtrade:2025.11
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: freqtrade
|
||||||
|
volumes:
|
||||||
|
- ./user_data:/freqtrade/user_data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8081:8080"
|
||||||
|
command: >
|
||||||
|
trade
|
||||||
|
--logfile /freqtrade/user_data/logs/freqtrade.log
|
||||||
|
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
|
||||||
|
--config /freqtrade/user_data/config.json
|
||||||
|
--config /freqtrade/user_data/config.pairlist.json --config /freqtrade/user_data/secrets.json
|
||||||
|
--strategy StructureFlowStrategyV22d
|
||||||
68
config/freqtrade/config.json
Normal file
68
config/freqtrade/config.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"max_open_trades": 1,
|
||||||
|
"stake_currency": "USDT",
|
||||||
|
"stake_amount": "unlimited",
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
|
"dry_run": true,
|
||||||
|
"dry_run_wallet": 1000,
|
||||||
|
"cancel_open_orders_on_exit": false,
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated",
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1
|
||||||
|
},
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"entry": 10,
|
||||||
|
"exit": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "binance",
|
||||||
|
"key": "",
|
||||||
|
"secret": "",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {}
|
||||||
|
},
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "StaticPairList",
|
||||||
|
"allow_inactive": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "8872284120:AAHZ9vbi_949uri-aZ4-BCIAqyDQ1OLuc7c",
|
||||||
|
"chat_id": "1436534523",
|
||||||
|
"notification_settings": {
|
||||||
|
"status": "on",
|
||||||
|
"warning": "on",
|
||||||
|
"startup": "on",
|
||||||
|
"entry": "on",
|
||||||
|
"entry_fill": "on",
|
||||||
|
"exit": "on",
|
||||||
|
"exit_fill": "on",
|
||||||
|
"exit_cancel": "on",
|
||||||
|
"protection_trigger": "on",
|
||||||
|
"protection_trigger_global": "on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bot_name": "freqtrade",
|
||||||
|
"initial_state": "running",
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
8
config/freqtrade/config.pairlist.json
Normal file
8
config/freqtrade/config.pairlist.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"exchange": {
|
||||||
|
"name": "binance",
|
||||||
|
"pair_whitelist": [
|
||||||
|
"ETH/USDT:USDT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
50
daily_briefs/archive/2026-06-08.txt
Normal file
50
daily_briefs/archive/2026-06-08.txt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
╔══════════════════════════════════════════════════════════╗
|
||||||
|
║ 📊 Structure Flow Strategy v2.1 — 每日运行简报 ║
|
||||||
|
║ 📅 2026年06月08日 23:55 (北京时间) ║
|
||||||
|
╚══════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
┌─ 🟢 一、Bot 运行状态────────────────────────────────┐
|
||||||
|
│ ✅ 状态: 运行中
|
||||||
|
│ ✅ 重启次数: 0 (稳定运行)
|
||||||
|
│ ⏱️ 今日运行: 5小时52分钟
|
||||||
|
│ 💓 最后心跳: 2026-06-08 23:54:26 (35秒前)
|
||||||
|
│ 💓 今日心跳: 349 次
|
||||||
|
│ ✅ 错误: 0
|
||||||
|
│ 🔍 白名单更新: 8 次
|
||||||
|
│ 📋 交易对: ETH/USDT:USDT
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 📡 二、入场筛选活动──────────────────────────────┐
|
||||||
|
│ 🔕 策略处于静默状态
|
||||||
|
│ 🎯 入场标准 (需同时满足):
|
||||||
|
│ ① D1趋势方向一致
|
||||||
|
│ ② 4H供需区位置正确
|
||||||
|
│ ③ K线形态确认 (Pin Bar / Engulfing)
|
||||||
|
│ ④ 止损距离在合理范围内
|
||||||
|
│ ⑤ 支撑/阻力存活 (近期被测试过)
|
||||||
|
│ ⑥ 4H趋势强度在扩张
|
||||||
|
│ ⑦ 冷却期内无重复信号
|
||||||
|
│ ℹ️ 静默 = 上述条件未同时满足 → 策略在保护资金
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 💰 三、交易活动──────────────────────────────────────┐
|
||||||
|
│ 📭 当前持仓: 无
|
||||||
|
│
|
||||||
|
│ 🆕 今日新开: 0 笔
|
||||||
|
│
|
||||||
|
│ 🔒 今日平仓: 0 笔
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 🏥 五、策略健康度────────────────────────────────────┐
|
||||||
|
│ ✅ 心跳正常
|
||||||
|
│ ✅ 零错误
|
||||||
|
│ ✅ 稳定运行
|
||||||
|
│ 🟢 健康度: 优秀 (100/100)
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
📌 策略: StructureFlowStrategyV21 | 模式: dry_run | 杠杆: 1x
|
||||||
|
📌 服务器: 43.163.225.30 | 日志: /home/ubuntu/freqtrade/user_data/logs/freqtrade.log
|
||||||
|
📌 报告生成: 2026-06-08 23:55:01 (北京时间)
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
52
daily_briefs/archive/2026-06-09.txt
Normal file
52
daily_briefs/archive/2026-06-09.txt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
╔══════════════════════════════════════════════════════════╗
|
||||||
|
║ 📊 Structure Flow Strategy v2.1 — 每日运行简报 ║
|
||||||
|
║ 📅 2026年06月09日 23:55 (北京时间) ║
|
||||||
|
╚══════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
┌─ 🟢 一、Bot 运行状态────────────────────────────────┐
|
||||||
|
│ ✅ 状态: 运行中
|
||||||
|
│ ✅ 重启次数: 0 (稳定运行)
|
||||||
|
│ ⏱️ 今日运行: 15小时54分钟
|
||||||
|
│ 💓 最后心跳: 2026-06-09 23:54:11 (51秒前)
|
||||||
|
│ 💓 今日心跳: 953 次
|
||||||
|
│ ❌ 错误: 2 条
|
||||||
|
│ └ 2026-06-09 01:11:28,692 - telegram.ext.Updater - ERROR - Exception happened whil
|
||||||
|
│ └ 2026-06-09 01:11:30,023 - telegram.ext.Updater - ERROR - Exception happened whil
|
||||||
|
│ 🔍 白名单更新: 15 次
|
||||||
|
│ 📋 交易对: ETH/USDT:USDT
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 📡 二、入场筛选活动──────────────────────────────┐
|
||||||
|
│ 🔕 策略处于静默状态
|
||||||
|
│ 🎯 入场标准 (需同时满足):
|
||||||
|
│ ① D1趋势方向一致
|
||||||
|
│ ② 4H供需区位置正确
|
||||||
|
│ ③ K线形态确认 (Pin Bar / Engulfing)
|
||||||
|
│ ④ 止损距离在合理范围内
|
||||||
|
│ ⑤ 支撑/阻力存活 (近期被测试过)
|
||||||
|
│ ⑥ 4H趋势强度在扩张
|
||||||
|
│ ⑦ 冷却期内无重复信号
|
||||||
|
│ ℹ️ 静默 = 上述条件未同时满足 → 策略在保护资金
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 💰 三、交易活动──────────────────────────────────────┐
|
||||||
|
│ 📭 当前持仓: 无
|
||||||
|
│
|
||||||
|
│ 🆕 今日新开: 0 笔
|
||||||
|
│
|
||||||
|
│ 🔒 今日平仓: 0 笔
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 🏥 五、策略健康度────────────────────────────────────┐
|
||||||
|
│ ✅ 心跳正常
|
||||||
|
│ ⚡ 轻微错误: 2 条 (不影响运行)
|
||||||
|
│ ✅ 稳定运行
|
||||||
|
│ 🟢 健康度: 优秀 (100/100)
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
📌 策略: StructureFlowStrategyV21 | 模式: dry_run | 杠杆: 1x
|
||||||
|
📌 服务器: 43.163.225.30 | 日志: /home/ubuntu/freqtrade/user_data/logs/freqtrade.log
|
||||||
|
📌 报告生成: 2026-06-09 23:55:02 (北京时间)
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
50
daily_briefs/archive/2026-06-10.txt
Normal file
50
daily_briefs/archive/2026-06-10.txt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
╔══════════════════════════════════════════════════════════╗
|
||||||
|
║ 📊 Structure Flow Strategy v2.1 — 每日运行简报 ║
|
||||||
|
║ 📅 2026年06月10日 23:55 (北京时间) ║
|
||||||
|
╚══════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
┌─ 🟢 一、Bot 运行状态────────────────────────────────┐
|
||||||
|
│ ✅ 状态: 运行中
|
||||||
|
│ ✅ 重启次数: 0 (稳定运行)
|
||||||
|
│ ⏱️ 今日运行: 15小时54分钟
|
||||||
|
│ 💓 最后心跳: 2026-06-10 23:54:32 (29秒前)
|
||||||
|
│ 💓 今日心跳: 953 次
|
||||||
|
│ ✅ 错误: 0
|
||||||
|
│ 🔍 白名单更新: 16 次
|
||||||
|
│ 📋 交易对: ETH/USDT:USDT
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 📡 二、入场筛选活动──────────────────────────────┐
|
||||||
|
│ 🔕 策略处于静默状态
|
||||||
|
│ 🎯 入场标准 (需同时满足):
|
||||||
|
│ ① D1趋势方向一致
|
||||||
|
│ ② 4H供需区位置正确
|
||||||
|
│ ③ K线形态确认 (Pin Bar / Engulfing)
|
||||||
|
│ ④ 止损距离在合理范围内
|
||||||
|
│ ⑤ 支撑/阻力存活 (近期被测试过)
|
||||||
|
│ ⑥ 4H趋势强度在扩张
|
||||||
|
│ ⑦ 冷却期内无重复信号
|
||||||
|
│ ℹ️ 静默 = 上述条件未同时满足 → 策略在保护资金
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 💰 三、交易活动──────────────────────────────────────┐
|
||||||
|
│ 📭 当前持仓: 无
|
||||||
|
│
|
||||||
|
│ 🆕 今日新开: 0 笔
|
||||||
|
│
|
||||||
|
│ 🔒 今日平仓: 0 笔
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ 🏥 五、策略健康度────────────────────────────────────┐
|
||||||
|
│ ✅ 心跳正常
|
||||||
|
│ ✅ 零错误
|
||||||
|
│ ✅ 稳定运行
|
||||||
|
│ 🟢 健康度: 优秀 (100/100)
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
|
📌 策略: StructureFlowStrategyV21 | 模式: dry_run | 杠杆: 1x
|
||||||
|
📌 服务器: 43.163.225.30 | 日志: /home/ubuntu/freqtrade/user_data/logs/freqtrade.log
|
||||||
|
📌 报告生成: 2026-06-10 23:55:01 (北京时间)
|
||||||
|
────────────────────────────────────────────────────────────
|
||||||
BIN
dashboard/__pycache__/indicators.cpython-312.pyc
Normal file
BIN
dashboard/__pycache__/indicators.cpython-312.pyc
Normal file
Binary file not shown.
BIN
dashboard/__pycache__/main.cpython-312.pyc
Normal file
BIN
dashboard/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
834
dashboard/index.html
Normal file
834
dashboard/index.html
Normal file
@ -0,0 +1,834 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||||
|
<meta name="theme-color" content="#0d1117">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<title>Beast Trader</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
:root{
|
||||||
|
--bg:#0d1117;--surface:#161b22;--surface2:#21262d;
|
||||||
|
--border:#30363d;--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
|
||||||
|
--green:#3fb950;--red:#f85149;--yellow:#d29922;--blue:#58a6ff;
|
||||||
|
}
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);font-size:14px;padding-bottom:70px;overflow-x:hidden}
|
||||||
|
.header{padding:16px 16px 8px;display:flex;justify-content:space-between;align-items:center;border-bottom:0.5px solid var(--border);background:var(--surface)}
|
||||||
|
.header h1{font-size:18px;font-weight:500}
|
||||||
|
.header .sub{font-size:12px;color:var(--text2)}
|
||||||
|
.st-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
|
||||||
|
.st-ok{background:var(--green)}.st-warn{background:var(--yellow)}.st-err{background:var(--red)}
|
||||||
|
.tabs{display:flex;background:var(--surface);border-bottom:0.5px solid var(--border);position:sticky;top:0;z-index:10}
|
||||||
|
.tab{flex:1;text-align:center;padding:12px;font-size:13px;color:var(--text2);cursor:pointer;border-bottom:2px solid transparent}
|
||||||
|
.tab.active{color:var(--blue);border-bottom-color:var(--blue)}
|
||||||
|
.tc{display:none;padding:12px 16px}.tc.active{display:block}
|
||||||
|
.mg{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
|
||||||
|
.mc{background:var(--surface);border-radius:10px;padding:12px;border:0.5px solid var(--border)}
|
||||||
|
.mc .l{font-size:11px;color:var(--text3);margin-bottom:4px}
|
||||||
|
.mc .v{font-size:18px;font-weight:500}.v.gn{color:var(--green)}.v.rd{color:var(--red)}.v.bl{color:var(--blue)}.v.yl{color:var(--yellow)}
|
||||||
|
.ca{background:var(--surface);border-radius:10px;padding:12px;margin-bottom:12px;border:0.5px solid var(--border);overflow:hidden}
|
||||||
|
.ca .ct{font-size:13px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between}
|
||||||
|
svg{width:100%;height:auto}
|
||||||
|
.sr-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:500}
|
||||||
|
.sr-support{background:rgba(63,185,80,0.15);color:var(--green)}
|
||||||
|
.sr-resist{background:rgba(248,81,73,0.15);color:var(--red)}
|
||||||
|
.sr-price{background:rgba(88,166,255,0.15);color:var(--blue)}
|
||||||
|
.sr-row{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
|
||||||
|
.fl{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}
|
||||||
|
.fi{background:var(--surface);border-radius:8px;padding:10px 12px;border:0.5px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
||||||
|
.fi .nm{font-size:13px;font-weight:500}.fi .ds{font-size:11px;color:var(--text3);margin-top:2px}
|
||||||
|
.fi .st{font-size:12px;font-weight:500;padding:3px 8px;border-radius:5px}
|
||||||
|
.st-p{background:rgba(63,185,80,0.2);color:var(--green)}
|
||||||
|
.st-f{background:rgba(248,81,73,0.2);color:var(--red)}
|
||||||
|
.cb{padding:12px;border-radius:10px;margin-bottom:12px;text-align:center;font-size:15px;font-weight:500}
|
||||||
|
.cb.rdy{background:rgba(63,185,80,0.15);color:var(--green);border:0.5px solid var(--green)}
|
||||||
|
.cb.wt{background:rgba(210,153,34,0.15);color:var(--yellow);border:0.5px solid var(--yellow)}
|
||||||
|
.cb.no{background:rgba(248,81,73,0.1);color:var(--text3);border:0.5px solid var(--border)}
|
||||||
|
.ftr{position:fixed;bottom:0;left:0;right:0;background:var(--surface);border-top:0.5px solid var(--border);padding:8px 16px;display:flex;justify-content:space-between;align-items:center;z-index:20}
|
||||||
|
.ftr .fi2{font-size:11px;color:var(--text3)}
|
||||||
|
.rf{background:var(--surface2);border:0.5px solid var(--border);color:var(--text);padding:6px 16px;border-radius:8px;font-size:13px;cursor:pointer}
|
||||||
|
.sym{display:flex;gap:6px;margin-bottom:12px}
|
||||||
|
.sb{flex:1;padding:8px;text-align:center;border-radius:8px;border:0.5px solid var(--border);font-size:13px;cursor:pointer;color:var(--text2);background:var(--surface)}
|
||||||
|
.sb.ac{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,0.1)}
|
||||||
|
.ta{font-size:16px}.ta.up{color:var(--green)}.ta.dn{color:var(--red)}.ta.nt{color:var(--text3)}
|
||||||
|
.leg{display:flex;gap:12px;font-size:11px;color:var(--text3);margin-bottom:4px}
|
||||||
|
.leg span{display:flex;align-items:center;gap:4px}
|
||||||
|
.dot{width:8px;height:8px;border-radius:50%;display:inline-block}.dot.gn{background:var(--green)}.dot.rd{background:var(--red)}
|
||||||
|
.ld{text-align:center;padding:30px;color:var(--text3);font-size:14px}
|
||||||
|
.er{text-align:center;padding:20px;color:var(--red);font-size:13px}
|
||||||
|
.sec-title{font-size:12px;font-weight:500;color:var(--text2);margin:8px 0 4px}
|
||||||
|
.tr{background:var(--surface);border-radius:8px;padding:10px 12px;margin-bottom:6px;border:0.5px solid var(--border);font-size:12px}
|
||||||
|
.tr .tp{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
|
||||||
|
.tr .td{display:flex;gap:16px;color:var(--text3);font-size:11px}
|
||||||
|
.tr .pnl{font-weight:600;font-size:13px}
|
||||||
|
.tbl{width:100%;border-collapse:collapse;font-size:11px}
|
||||||
|
.tbl th{color:var(--text3);font-weight:400;text-align:left;padding:4px 8px;border-bottom:0.5px solid var(--border)}
|
||||||
|
.tbl td{padding:6px 8px;border-bottom:0.5px solid var(--border);color:var(--text2)}
|
||||||
|
.tbl .num{text-align:right}
|
||||||
|
.tbl .win{color:var(--green)}.tbl .lose{color:var(--red)}
|
||||||
|
.pos-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500}
|
||||||
|
.pos-long{background:rgba(63,185,80,0.2);color:var(--green)}
|
||||||
|
.pos-short{background:rgba(248,81,73,0.2);color:var(--red)}
|
||||||
|
.empty-state{text-align:center;padding:24px;color:var(--text3);font-size:13px}
|
||||||
|
.g3{grid-template-columns:1fr 1fr 1fr}
|
||||||
|
.g4{grid-template-columns:1fr 1fr 1fr 1fr}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div>
|
||||||
|
<h1>Beast Trader</h1>
|
||||||
|
<div class="sub">v2.2d · Dashboard V1.2 · 直连 Binance</div>
|
||||||
|
</div>
|
||||||
|
<div><span class="st-dot st-warn" id="stDot"></span><span style="font-size:12px;color:var(--text2)" id="stTxt">初始化</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="csBanner" style="display:none;background:rgba(248,81,73,0.15);border:1px solid var(--red);border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:12px;color:var(--red);text-align:center">
|
||||||
|
⚠️ CloudStudio 不支持 HTTPS→HTTP 请求,请用手机浏览器打开<br>
|
||||||
|
<a href="http://43.163.225.30:9000/" style="color:var(--green);text-decoration:underline">http://43.163.225.30:9000/</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active" onclick="swTab('st')">结构分析</div>
|
||||||
|
<div class="tab" onclick="swTab('diag')">信号诊断</div>
|
||||||
|
<div class="tab" onclick="swTab('trade')">交易监测</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tc active" id="tab-st">
|
||||||
|
<div class="sym">
|
||||||
|
<div class="sb ac" onclick="swSym('ETH/USDT')">ETH/USDT</div>
|
||||||
|
<div class="sb" onclick="swSym('BTC/USDT')">BTC/USDT</div>
|
||||||
|
</div>
|
||||||
|
<div class="mg" id="mGrid">
|
||||||
|
<div class="mc"><div class="l">当前价格</div><div class="v bl" id="cPrice">--</div></div>
|
||||||
|
<div class="mc"><div class="l">D1 趋势</div><div class="v" id="cTrend"><span class="ta nt">→</span> <span>计算中</span></div></div>
|
||||||
|
<div class="mc"><div class="l">支撑</div><div class="v gn" id="cSup">--</div></div>
|
||||||
|
<div class="mc"><div class="l">阻力</div><div class="v rd" id="cRes">--</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="sr-row" id="srRow"></div>
|
||||||
|
<div class="ca">
|
||||||
|
<div class="ct"><span id="chP">ETH/USDT</span><span style="color:var(--text3);font-size:11px">最近 48 根 1H K线</span></div>
|
||||||
|
<div class="leg"><span><span class="dot gn"></span>Swing High</span><span><span class="dot rd"></span>Swing Low</span></div>
|
||||||
|
<svg viewBox="0 0 340 160" id="chSvg"><rect width="340" height="160" fill="var(--surface)" rx="6"/><text x="170" y="80" text-anchor="middle" fill="var(--text3)" font-size="12">获取数据中...</text></svg>
|
||||||
|
</div>
|
||||||
|
<div class="ca">
|
||||||
|
<div class="ct"><span>条件检查</span><span style="color:var(--text3);font-size:11px">实时</span></div>
|
||||||
|
<div id="condCard"><div class="ld">加载中...</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tc" id="tab-diag">
|
||||||
|
<div style="margin-bottom:8px;font-size:13px;color:var(--text2)">ETH/USDT · 实时过滤条件检查</div>
|
||||||
|
<div id="conclBar"></div>
|
||||||
|
<div class="fl" id="fList"><div class="ld">加载中...</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tc" id="tab-trade">
|
||||||
|
<div class="mg g4" id="accGrid">
|
||||||
|
<div class="mc"><div class="l">当前余额</div><div class="v bl" id="accBalance">--</div></div>
|
||||||
|
<div class="mc"><div class="l">总收益</div><div class="v" id="accPnl">--</div></div>
|
||||||
|
<div class="mc"><div class="l">胜率</div><div class="v" id="accWinRate">--</div></div>
|
||||||
|
<div class="mc"><div class="l">交易数</div><div class="v" id="accTrades">--</div></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:16px;margin-bottom:8px;font-size:11px;color:var(--text3)">
|
||||||
|
<span id="accStrategy">策略: --</span>
|
||||||
|
<span id="accUpdate">更新: --</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec-title">📌 当前持仓</div>
|
||||||
|
<div id="openPos"><div class="ld">加载中...</div></div>
|
||||||
|
|
||||||
|
<div class="sec-title">📋 近期交易 (<span id="tradeTotal">0</span>笔)</div>
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table class="tbl"><thead>
|
||||||
|
<tr><th>时间</th><th>方向</th><th>入场价</th><th>出场价</th><th class="num">盈亏</th><th>原因</th></tr>
|
||||||
|
</thead><tbody id="tradeBody"><tr><td colspan="6" class="empty-state">暂无交易记录</td></tr></tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec-title" style="margin-top:12px">📊 每日盈亏 (近30天)</div>
|
||||||
|
<div class="ca">
|
||||||
|
<svg viewBox="0 0 340 120" id="pnlSvg"><rect width="340" height="120" fill="var(--surface)" rx="6"/><text x="170" y="60" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec-title">📉 盈亏分布</div>
|
||||||
|
<div class="ca">
|
||||||
|
<svg viewBox="0 0 340 100" id="distSvg"><rect width="340" height="100" fill="var(--surface)" rx="6"/><text x="170" y="50" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec-title" style="margin-top:12px">📊 回测年化表现(2021-2026)</div>
|
||||||
|
<div style="font-size:12px;color:var(--text2);margin-bottom:12px">
|
||||||
|
<table style="width:100%;border-collapse:collapse;font-size:12px">
|
||||||
|
<tr style="background:var(--surface2)">
|
||||||
|
<th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)">年份</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right;border-bottom:1px solid var(--border)">交易数</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right;border-bottom:1px solid var(--border)">收益率</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right;border-bottom:1px solid var(--border)">终值</th>
|
||||||
|
</tr>
|
||||||
|
<tr><td style="padding:6px 8px">2021</td><td style="padding:6px 8px;text-align:right">172</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+251.16%</td><td style="padding:6px 8px;text-align:right">$35,116</td></tr>
|
||||||
|
<tr style="background:var(--surface2)"><td style="padding:6px 8px">2022</td><td style="padding:6px 8px;text-align:right">204</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+110.91%</td><td style="padding:6px 8px;text-align:right">$21,091</td></tr>
|
||||||
|
<tr><td style="padding:6px 8px">2023</td><td style="padding:6px 8px;text-align:right">182</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+49.35%</td><td style="padding:6px 8px;text-align:right">$14,935</td></tr>
|
||||||
|
<tr style="background:var(--surface2)"><td style="padding:6px 8px">2024</td><td style="padding:6px 8px;text-align:right">232</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+185.84%</td><td style="padding:6px 8px;text-align:right">$28,584</td></tr>
|
||||||
|
<tr><td style="padding:6px 8px">2025</td><td style="padding:6px 8px;text-align:right">221</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+608.24%</td><td style="padding:6px 8px;text-align:right">$70,824</td></tr>
|
||||||
|
<tr style="background:var(--surface2)"><td style="padding:6px 8px">2026</td><td style="padding:6px 8px;text-align:right">54</td><td style="padding:6px 8px;text-align:right;color:var(--dn)">-11.87%</td><td style="padding:6px 8px;text-align:right">$8,813</td></tr>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top:6px;padding:6px 8px;background:var(--surface2);border-radius:4px">
|
||||||
|
<b>v2.2d 全周期 (2021-2026) 总收益:+205,684%</b> · CAGR 309.01% · Sharpe 1.03 · 最大回撤20.58%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ftr">
|
||||||
|
<span class="fi2" id="ftInfo">上次更新: --</span>
|
||||||
|
<button class="rf" onclick="refresh()">刷新</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ============================================================
|
||||||
|
// 指标计算(移植自 v2.2b 策略)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function detectSwingPoints(high, low, window=5) {
|
||||||
|
const sh = new Array(high.length).fill(NaN);
|
||||||
|
const sl = new Array(low.length).fill(NaN);
|
||||||
|
for (let i = window; i < high.length - window; i++) {
|
||||||
|
const leftH = high.slice(i - window, i);
|
||||||
|
const rightH = high.slice(i + 1, i + window + 1);
|
||||||
|
if (high[i] > Math.max(...leftH) && high[i] > Math.max(...rightH)) sh[i] = high[i];
|
||||||
|
const leftL = low.slice(i - window, i);
|
||||||
|
const rightL = low.slice(i + 1, i + window + 1);
|
||||||
|
if (low[i] < Math.min(...leftL) && low[i] < Math.min(...rightL)) sl[i] = low[i];
|
||||||
|
}
|
||||||
|
return [sh, sl];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStructure(high, low, close, sh, sl) {
|
||||||
|
const n = high.length;
|
||||||
|
const tu = new Array(n).fill(false);
|
||||||
|
const td = new Array(n).fill(false);
|
||||||
|
const sup = new Array(n).fill(NaN);
|
||||||
|
const res = new Array(n).fill(NaN);
|
||||||
|
const idm = new Array(n).fill(false);
|
||||||
|
const isp = new Array(n).fill(false);
|
||||||
|
const shP = [], slP = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (!isNaN(sh[i])) { shP.push(sh[i]); if (shP.length > 4) shP.shift(); }
|
||||||
|
if (!isNaN(sl[i])) { slP.push(sl[i]); if (slP.length > 4) slP.shift(); }
|
||||||
|
if (shP.length >= 2 && slP.length >= 2) {
|
||||||
|
if (shP[shP.length-1] > shP[shP.length-2] && slP[slP.length-1] > slP[slP.length-2]) tu[i] = true;
|
||||||
|
else if (shP[shP.length-1] < shP[shP.length-2] && slP[slP.length-1] < slP[slP.length-2]) td[i] = true;
|
||||||
|
else if (i > 0) { tu[i] = tu[i-1]; td[i] = td[i-1]; }
|
||||||
|
} else if (i > 0) { tu[i] = tu[i-1]; td[i] = td[i-1]; }
|
||||||
|
if (slP.length) sup[i] = slP[slP.length-1];
|
||||||
|
if (shP.length) res[i] = shP[shP.length-1];
|
||||||
|
const c = close[i];
|
||||||
|
if (!isNaN(sup[i]) && !isNaN(res[i]) && res[i] - sup[i] > 0) {
|
||||||
|
const pct = (c - sup[i]) / (res[i] - sup[i]);
|
||||||
|
idm[i] = pct < 0.35;
|
||||||
|
isp[i] = pct > 0.65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {tu, td, sup, res, idm, isp};
|
||||||
|
}
|
||||||
|
|
||||||
|
function trendStrength(high, low, sh, sl, minStr=-0.20) {
|
||||||
|
const n = high.length;
|
||||||
|
const up = new Array(n).fill(NaN), dn = new Array(n).fill(NaN);
|
||||||
|
const supS = new Array(n).fill(false), sdn = new Array(n).fill(false);
|
||||||
|
const shP = [], slP = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (!isNaN(sh[i])) { shP.push(sh[i]); if (shP.length > 4) shP.shift(); }
|
||||||
|
if (!isNaN(sl[i])) { slP.push(sl[i]); if (slP.length > 4) slP.shift(); }
|
||||||
|
if (shP.length >= 2 && slP.length >= 2) {
|
||||||
|
const hhD = (shP[shP.length-1] - shP[shP.length-2]) / shP[shP.length-2];
|
||||||
|
const hlD = (slP[slP.length-1] - slP[slP.length-2]) / slP[slP.length-2];
|
||||||
|
up[i] = hhD + hlD;
|
||||||
|
dn[i] = -(hhD + hlD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
supS[i] = !isNaN(up[i]) && up[i] > minStr;
|
||||||
|
sdn[i] = !isNaN(dn[i]) && dn[i] > minStr;
|
||||||
|
}
|
||||||
|
return {up, dn, supS, sdn};
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportAlive(low, close, support) {
|
||||||
|
const n = low.length;
|
||||||
|
const r = new Array(n).fill(false);
|
||||||
|
let found = false;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (isNaN(support[i])) { r[i] = found; continue; }
|
||||||
|
const touched = low[i] <= support[i] * 1.005 && low[i] >= support[i] * 0.995;
|
||||||
|
const held = close[i] > support[i];
|
||||||
|
if (touched && held) found = true;
|
||||||
|
r[i] = found;
|
||||||
|
if (i >= 3 && !touched) {
|
||||||
|
let keep = false;
|
||||||
|
for (let j = Math.max(0, i-2); j <= i; j++) {
|
||||||
|
const tj = low[j] <= support[j] * 1.005 && low[j] >= support[j] * 0.995;
|
||||||
|
const hj = close[j] > support[j];
|
||||||
|
if (tj && hj) keep = true;
|
||||||
|
}
|
||||||
|
r[i] = keep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resistAlive(high, close, resist) {
|
||||||
|
const n = high.length;
|
||||||
|
const r = new Array(n).fill(false);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
if (isNaN(resist[i])) { r[i] = i > 0 ? r[i-1] : false; continue; }
|
||||||
|
let found = false;
|
||||||
|
for (let j = Math.max(0, i-2); j <= i; j++) {
|
||||||
|
const touched = high[j] >= resist[j] * 0.995 && high[j] <= resist[j] * 1.005;
|
||||||
|
const held = close[j] < resist[j];
|
||||||
|
if (touched && held) found = true;
|
||||||
|
}
|
||||||
|
r[i] = found;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeAll(pair, df1h, df4h, df1d) {
|
||||||
|
const params = {swingD1:10, swingH4:8, pbr:0.6, msd:0.50, tsm:-0.20};
|
||||||
|
|
||||||
|
// D1
|
||||||
|
const [sh1d, sl1d] = detectSwingPoints(df1d.high, df1d.low, params.swingD1);
|
||||||
|
const st1d = buildStructure(df1d.high, df1d.low, df1d.close, sh1d, sl1d);
|
||||||
|
const li1d = st1d.tu.length-1;
|
||||||
|
const trUp1d = st1d.tu[li1d], trDn1d = st1d.td[li1d];
|
||||||
|
|
||||||
|
// 4H
|
||||||
|
const [sh4h, sl4h] = detectSwingPoints(df4h.high, df4h.low, params.swingH4);
|
||||||
|
const st4h = buildStructure(df4h.high, df4h.low, df4h.close, sh4h, sl4h);
|
||||||
|
const ts4h = trendStrength(df4h.high, df4h.low, sh4h, sl4h, params.tsm);
|
||||||
|
const li4h = st4h.tu.length-1;
|
||||||
|
const sup4h = st4h.sup[li4h], res4h = st4h.res[li4h];
|
||||||
|
const idx4h = st4h.idm[li4h], isp4h = st4h.isp[li4h];
|
||||||
|
const sa4h = supportAlive(df4h.low, df4h.close, st4h.sup)[li4h];
|
||||||
|
const ra4h = resistAlive(df4h.high, df4h.close, st4h.res)[li4h];
|
||||||
|
const sUp4h = ts4h.supS[li4h], sDn4h = ts4h.sdn[li4h];
|
||||||
|
const tsUp = ts4h.up[li4h], tsDn = ts4h.dn[li4h];
|
||||||
|
|
||||||
|
// 1H
|
||||||
|
const lh = df1h.close.length-1;
|
||||||
|
const cPrice = df1h.close[lh];
|
||||||
|
const bod = df1h.close.map((c,i) => Math.abs(c - df1h.open[i]));
|
||||||
|
const tr = df1h.high.map((h,i) => Math.max(h - df1h.low[i], 0.0001));
|
||||||
|
const uw = df1h.high.map((h,i) => h - (df1h.close[i] > df1h.open[i] ? df1h.close[i] : df1h.open[i]));
|
||||||
|
const lw = df1h.high.map((h,i) => (df1h.close[i] > df1h.open[i] ? df1h.open[i] : df1h.close[i]) - df1h.low[i]);
|
||||||
|
const isPin = tr.map((t,i) => (uw[i] + lw[i]) / t > params.pbr);
|
||||||
|
const bp = isPin.map((p,i) => p && df1h.close[i] > df1h.open[i] && lw[i] > uw[i]);
|
||||||
|
const bsh = isPin.map((p,i) => p && df1h.close[i] < df1h.open[i] && uw[i] > lw[i]);
|
||||||
|
const be = df1h.close.map((c,i) => i>0 && c > df1h.open[i-1] && df1h.open[i] < df1h.close[i-1] && c > df1h.open[i]);
|
||||||
|
const bse = df1h.close.map((c,i) => i>0 && c < df1h.open[i-1] && df1h.open[i] > df1h.close[i-1] && c < df1h.open[i]);
|
||||||
|
const buS = bp.map((p,i) => p || (be[i]||false));
|
||||||
|
const beS = bsh.map((p,i) => p || (bse[i]||false));
|
||||||
|
|
||||||
|
// Distances
|
||||||
|
const lsd = !isNaN(sup4h) && sup4h > 0 ? (cPrice - sup4h) / cPrice : null;
|
||||||
|
const ssd = !isNaN(res4h) && res4h > 0 ? (res4h - cPrice) / cPrice : null;
|
||||||
|
const ldOk = lsd !== null && lsd <= params.msd && lsd > 0.003;
|
||||||
|
const sdOk = ssd !== null && ssd <= params.msd && ssd > 0.003;
|
||||||
|
|
||||||
|
// Zone info
|
||||||
|
let zoneW = null, posPct = null;
|
||||||
|
if (!isNaN(sup4h) && !isNaN(res4h) && sup4h > 0 && res4h > sup4h) {
|
||||||
|
zoneW = ((res4h - sup4h) / sup4h) * 100;
|
||||||
|
posPct = ((cPrice - sup4h) / (res4h - sup4h)) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swing points history
|
||||||
|
const sHighs = [], sLows = [];
|
||||||
|
for (let i = 0; i < sh4h.length; i++) {
|
||||||
|
if (!isNaN(sh4h[i])) sHighs.push({price:sh4h[i], time:df4h.time[i]});
|
||||||
|
if (!isNaN(sl4h[i])) sLows.push({price:sl4h[i], time:df4h.time[i]});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnosis
|
||||||
|
const longOk = trUp1d && idx4h && ldOk && sa4h && sUp4h;
|
||||||
|
const shortOk = trDn1d && isp4h && sdOk && ra4h && sDn4h;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cPrice: Math.round(cPrice * 100) / 100,
|
||||||
|
sup4h: !isNaN(sup4h) ? Math.round(sup4h * 100) / 100 : null,
|
||||||
|
res4h: !isNaN(res4h) ? Math.round(res4h * 100) / 100 : null,
|
||||||
|
zoneW: zoneW !== null ? Math.round(zoneW * 100) / 100 : null,
|
||||||
|
posPct: posPct !== null ? Math.round(posPct * 10) / 10 : null,
|
||||||
|
trUp1d: trUp1d, trDn1d: trDn1d,
|
||||||
|
idx4h, isp4h, sa4h, ra4h, sUp4h, sDn4h,
|
||||||
|
tsUp: !isNaN(tsUp) ? Math.round(tsUp * 10000) / 100 : null,
|
||||||
|
tsDn: !isNaN(tsDn) ? Math.round(tsDn * 10000) / 100 : null,
|
||||||
|
lsd: lsd !== null ? Math.round(lsd * 10000) / 100 : null,
|
||||||
|
ssd: ssd !== null ? Math.round(ssd * 10000) / 100 : null,
|
||||||
|
ldOk, sdOk, longOk, shortOk,
|
||||||
|
buS: buS[lh], beS: beS[lh],
|
||||||
|
priceHist: df1h.close,
|
||||||
|
sHighs: sHighs.slice(-6), sLows: sLows.slice(-6),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 数据获取 (通过服务器后端 API)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
let cache = {};
|
||||||
|
let curSym = 'ETH/USDT';
|
||||||
|
const IS_CLOUDSTUDIO = window.location.hostname.includes('codebuddy.work') || window.location.hostname.includes('app.codebuddy');
|
||||||
|
const SERVER_URL = IS_CLOUDSTUDIO ? 'http://43.163.225.30:9000' : window.location.origin;
|
||||||
|
const API_KEY = 'beast2025';
|
||||||
|
|
||||||
|
async function fetchFromServer() {
|
||||||
|
const headers = { 'X-API-Key': API_KEY, 'Accept': 'application/json' };
|
||||||
|
const [structRes, diagRes] = await Promise.all([
|
||||||
|
fetch(SERVER_URL + '/api/market-structure', { headers }),
|
||||||
|
fetch(SERVER_URL + '/api/signal-diagnosis', { headers }),
|
||||||
|
]);
|
||||||
|
if (!structRes.ok || !diagRes.ok) throw new Error('API ' + structRes.status);
|
||||||
|
const struct = await structRes.json();
|
||||||
|
const diag = await diagRes.json();
|
||||||
|
return { struct, diag };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
document.getElementById('stDot').className = 'st-dot st-warn';
|
||||||
|
document.getElementById('stTxt').textContent = '获取数据';
|
||||||
|
try {
|
||||||
|
const data = await fetchFromServer();
|
||||||
|
const symKey = curSym === 'ETH/USDT' ? 'eth' : 'btc';
|
||||||
|
cache[curSym] = data;
|
||||||
|
cache[curSym + '_key'] = symKey;
|
||||||
|
document.getElementById('stDot').className = 'st-dot st-ok';
|
||||||
|
document.getElementById('stTxt').textContent = '在线';
|
||||||
|
render();
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('stDot').className = 'st-dot st-err';
|
||||||
|
if (e.message && (e.message.includes('Failed to fetch') || e.message.includes('NetworkError'))) {
|
||||||
|
document.getElementById('stTxt').textContent = IS_CLOUDSTUDIO ? 'HTTPS阻截' : '连接失败';
|
||||||
|
document.getElementById('csBanner').style.display = IS_CLOUDSTUDIO ? 'block' : 'none';
|
||||||
|
document.getElementById('mGrid').innerHTML = '<div class="er">⚠️ 无法连接服务器<br><small>请用手机浏览器直接打开<br><a href="http://43.163.225.30:9000/" style="color:var(--green)">http://43.163.225.30:9000/</a></small></div>';
|
||||||
|
document.getElementById('fList').innerHTML = '<div class="er">连接失败 — 请用直连 IP 访问</div>';
|
||||||
|
} else {
|
||||||
|
document.getElementById('stTxt').textContent = '离线';
|
||||||
|
document.getElementById('mGrid').innerHTML = '<div class="er">数据获取失败: ' + e.message + '</div>';
|
||||||
|
document.getElementById('fList').innerHTML = '<div class="er">连接失败</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 渲染(适配服务器 API 格式)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const raw = cache[curSym];
|
||||||
|
if (!raw) return;
|
||||||
|
const key = cache[curSym + '_key'] || 'eth';
|
||||||
|
const st = raw.struct ? raw.struct[key] : null;
|
||||||
|
const di = raw.diag ? raw.diag[key] : null;
|
||||||
|
if (!st || !di) return;
|
||||||
|
|
||||||
|
// Price cards
|
||||||
|
document.getElementById('cPrice').textContent = st.current_price ? '$' + Number(st.current_price).toLocaleString('en-US',{minimumFractionDigits:2}) : '--';
|
||||||
|
const tEl = document.getElementById('cTrend');
|
||||||
|
if (st.trend_1d === 'up') tEl.innerHTML = '<span class="ta up">↑</span> 上升趋势';
|
||||||
|
else if (st.trend_1d === 'down') tEl.innerHTML = '<span class="ta dn">↓</span> 下降趋势';
|
||||||
|
else tEl.innerHTML = '<span class="ta nt">→</span> 震荡';
|
||||||
|
document.getElementById('cSup').textContent = st.support ? '$' + Number(st.support).toLocaleString('en-US',{minimumFractionDigits:2}) : '--';
|
||||||
|
document.getElementById('cRes').textContent = st.resistance ? '$' + Number(st.resistance).toLocaleString('en-US',{minimumFractionDigits:2}) : '--';
|
||||||
|
|
||||||
|
// SR badges
|
||||||
|
let sr = '';
|
||||||
|
if (st.support) sr += '<span class="sr-badge sr-support">▼ 支撑 $' + Number(st.support).toFixed(2) + '</span>';
|
||||||
|
if (st.resistance) sr += '<span class="sr-badge sr-resist">▲ 阻力 $' + Number(st.resistance).toFixed(2) + '</span>';
|
||||||
|
if (st.zone_width_pct) sr += '<span class="sr-badge sr-price">◆ 带宽 ' + Number(st.zone_width_pct).toFixed(1) + '%</span>';
|
||||||
|
if (st.price_position_pct !== null && st.price_position_pct !== undefined) sr += '<span class="sr-badge sr-price">◆ 位置 ' + Number(st.price_position_pct).toFixed(0) + '%</span>';
|
||||||
|
document.getElementById('srRow').innerHTML = sr;
|
||||||
|
|
||||||
|
// Chart
|
||||||
|
try { renderChart(st); } catch(e) { console.error("Chart error:", e); }
|
||||||
|
|
||||||
|
// Condition cards
|
||||||
|
try { renderConditions(di, st); } catch(e) { console.error("Conditions error:", e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart(st) {
|
||||||
|
const hist = st.price_history_1h;
|
||||||
|
const p = hist ? hist.close : null;
|
||||||
|
const ts = hist ? hist.timestamps : null;
|
||||||
|
if (!p || p.length < 3) return;
|
||||||
|
|
||||||
|
const w = 340, h = 200, pad = 20, topPad = 22, botPad = 20;
|
||||||
|
const cw = w - pad*2, ch = h - topPad - botPad;
|
||||||
|
|
||||||
|
const vals = p.concat(st.support || []).concat(st.resistance || []);
|
||||||
|
const mn = Math.min(...vals), mx = Math.max(...vals);
|
||||||
|
const rg = (mx - mn) || 1;
|
||||||
|
|
||||||
|
const xS = (i) => pad + (i / (p.length - 1)) * cw;
|
||||||
|
const yS = (v) => topPad + ch - ((v - mn) / rg) * ch;
|
||||||
|
|
||||||
|
const pts = p.map((v,i) => xS(i).toFixed(0)+','+yS(v).toFixed(0)).join(' ');
|
||||||
|
|
||||||
|
let svg = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/>';
|
||||||
|
|
||||||
|
if (st.support && st.resistance && st.resistance > st.support) {
|
||||||
|
const supY = yS(st.support), resY = yS(st.resistance);
|
||||||
|
const zoneH = supY - resY;
|
||||||
|
svg += '<rect x="'+(w-12)+'" y="'+resY+'" width="6" height="'+zoneH+'" rx="3" fill="rgba(136,136,136,0.12)"/>';
|
||||||
|
const supplyH = zoneH * 0.35;
|
||||||
|
svg += '<rect x="'+(w-12)+'" y="'+resY+'" width="6" height="'+supplyH+'" rx="3" fill="rgba(248,81,73,0.2)"/>';
|
||||||
|
const demandH = zoneH * 0.35;
|
||||||
|
svg += '<rect x="'+(w-12)+'" y="'+(supY - demandH)+'" width="6" height="'+demandH+'" rx="3" fill="rgba(63,185,80,0.2)"/>';
|
||||||
|
|
||||||
|
svg += '<text x="'+pad+'" y="'+(resY + supplyH/2 + 3)+'" fill="var(--red)" font-size="9" opacity="0.6">供给区</text>';
|
||||||
|
svg += '<text x="'+pad+'" y="'+(supY - demandH/2 + 3)+'" fill="var(--green)" font-size="9" opacity="0.6">需求区</text>';
|
||||||
|
|
||||||
|
svg += '<line x1="'+pad+'" y1="'+resY.toFixed(0)+'" x2="'+(w-pad)+'" y2="'+resY.toFixed(0)+'" stroke="var(--red)" stroke-width="1" stroke-dasharray="4,3" opacity="0.8"/>';
|
||||||
|
svg += '<text x="'+(w-pad-14)+'" y="'+(resY-4)+'" text-anchor="end" fill="var(--red)" font-size="11">阻力 $'+Number(st.resistance).toFixed(0)+'</text>';
|
||||||
|
svg += '<line x1="'+pad+'" y1="'+supY.toFixed(0)+'" x2="'+(w-pad)+'" y2="'+supY.toFixed(0)+'" stroke="var(--green)" stroke-width="1" stroke-dasharray="4,3" opacity="0.8"/>';
|
||||||
|
svg += '<text x="'+(w-pad-14)+'" y="'+(supY-4)+'" text-anchor="end" fill="var(--green)" font-size="11">支撑 $'+Number(st.support).toFixed(0)+'</text>';
|
||||||
|
}
|
||||||
|
|
||||||
|
svg += '<polyline points="'+pts+'" fill="none" stroke="var(--blue)" stroke-width="1.5"/>';
|
||||||
|
|
||||||
|
const lx = xS(p.length-1), ly = yS(p[p.length-1]);
|
||||||
|
svg += '<circle cx="'+lx.toFixed(0)+'" cy="'+ly.toFixed(0)+'" r="3.5" fill="var(--blue)" stroke="var(--surface)" stroke-width="1.5"/>';
|
||||||
|
svg += '<text x="'+(lx).toFixed(0)+'" y="'+(ly-6).toFixed(0)+'" text-anchor="middle" fill="var(--blue)" font-size="10" font-weight="500">$'+Number(p[p.length-1]).toFixed(0)+'</text>';
|
||||||
|
|
||||||
|
if (st.swing_points) {
|
||||||
|
if (st.swing_points.highs) {
|
||||||
|
st.swing_points.highs.slice(-8).forEach(sp => {
|
||||||
|
const ix = p.findIndex(c => Math.abs(c - sp.price) / Math.max(sp.price,1) < 0.008);
|
||||||
|
if (ix >= 0) {
|
||||||
|
const sx = xS(ix), sy = yS(sp.price);
|
||||||
|
svg += '<circle cx="'+sx.toFixed(0)+'" cy="'+sy.toFixed(0)+'" r="2.5" fill="var(--green)" opacity="0.6"/>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (st.swing_points.lows) {
|
||||||
|
st.swing_points.lows.slice(-8).forEach(sp => {
|
||||||
|
const ix = p.findIndex(c => Math.abs(c - sp.price) / Math.max(sp.price,1) < 0.008);
|
||||||
|
if (ix >= 0) {
|
||||||
|
const sx = xS(ix), sy = yS(sp.price);
|
||||||
|
svg += '<circle cx="'+sx.toFixed(0)+'" cy="'+sy.toFixed(0)+'" r="2.5" fill="var(--red)" opacity="0.6"/>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg += '<text x="'+pad+'" y="'+(topPad-4)+'" fill="var(--text3)" font-size="9">$'+Number(mx).toFixed(0)+'</text>';
|
||||||
|
svg += '<text x="'+pad+'" y="'+(h-botPad+14)+'" fill="var(--text3)" font-size="9">$'+Number(mn).toFixed(0)+'</text>';
|
||||||
|
|
||||||
|
if (ts && ts.length > 1) {
|
||||||
|
svg += '<text x="'+pad+'" y="'+(h-2)+'" fill="var(--text3)" font-size="9">'+(ts[0]||'').slice(0,11).replace(' ',' ')+'</text>';
|
||||||
|
svg += '<text x="'+(w-pad)+'" y="'+(h-2)+'" text-anchor="end" fill="var(--text3)" font-size="9">'+(ts[ts.length-1]||'').slice(0,11).replace(' ',' ')+'</text>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('chSvg').innerHTML = svg;
|
||||||
|
document.getElementById('chP').textContent = curSym;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 条件卡片渲染(V1.2 — 同时显示做多+做空两套条件)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function renderConditions(di, st) {
|
||||||
|
const el = document.getElementById('condCard');
|
||||||
|
if (!di || !di.filters || !st) { el.innerHTML = '<div class="ld">加载中...</div>'; return; }
|
||||||
|
const f = di.filters;
|
||||||
|
|
||||||
|
const inSupply = f['in_supply_1h'] ? f['in_supply_1h'].pass : false;
|
||||||
|
const inDemand = f['in_demand_1h'] ? f['in_demand_1h'].pass : false;
|
||||||
|
const zoneLabel = inSupply ? '供给区' : (inDemand ? '需求区' : '中间区');
|
||||||
|
const zonePct = st.price_position_pct !== null && st.price_position_pct !== undefined ? st.price_position_pct : null;
|
||||||
|
|
||||||
|
const distToSup = st.current_price && st.support ? (st.current_price - st.support) : null;
|
||||||
|
const distToRes = st.resistance && st.current_price ? (st.resistance - st.current_price) : null;
|
||||||
|
|
||||||
|
const shortChecks = [
|
||||||
|
{key:'trend_down_1d', label:'D1 下降趋势'},
|
||||||
|
{key:'in_supply_1h', label:'在供给区'},
|
||||||
|
{key:'resistance_alive_1h', label:'阻力有效'},
|
||||||
|
{key:'strong_downtrend_4h', label:'4H 下降趋势'},
|
||||||
|
{key:'short_stop_distance', label:'止损距离合理'},
|
||||||
|
];
|
||||||
|
const longChecks = [
|
||||||
|
{key:'trend_up_1d', label:'D1 上升趋势'},
|
||||||
|
{key:'in_demand_1h', label:'在需求区'},
|
||||||
|
{key:'support_alive_1h', label:'支撑有效'},
|
||||||
|
{key:'strong_uptrend_4h', label:'4H 上升趋势'},
|
||||||
|
{key:'long_stop_distance', label:'止损距离合理'},
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderCheckGroup(checks, title, titleColor) {
|
||||||
|
let gHtml = '<div style="margin-bottom:8px">';
|
||||||
|
gHtml += '<div style="font-size:12px;font-weight:500;color:'+titleColor+';margin-bottom:4px">'+title+'</div>';
|
||||||
|
gHtml += '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">';
|
||||||
|
let passN = 0;
|
||||||
|
checks.forEach(c => {
|
||||||
|
const fl = f[c.key];
|
||||||
|
if (!fl) return;
|
||||||
|
if (fl.pass) passN++;
|
||||||
|
const bg = fl.pass ? 'rgba(63,185,80,0.12)' : 'rgba(248,81,73,0.08)';
|
||||||
|
const cl = fl.pass ? 'var(--green)' : 'var(--red)';
|
||||||
|
const icon = fl.pass ? '✓' : '✗';
|
||||||
|
gHtml += '<span style="padding:3px 8px;border-radius:4px;background:'+bg+';color:'+cl+';font-size:11px;font-weight:500">'+icon+' '+c.label+'</span>';
|
||||||
|
});
|
||||||
|
gHtml += '</div>';
|
||||||
|
const allPass = passN === checks.length;
|
||||||
|
const barCls = allPass ? 'cb rdy' : 'cb no';
|
||||||
|
gHtml += '<div class="'+barCls+'" style="margin-bottom:0;font-size:13px;padding:8px">'+(allPass ? '✅ '+title+'条件全部通过' : '❌ '+title+'条件 '+passN+'/'+checks.length+' 通过')+'</div>';
|
||||||
|
gHtml += '</div>';
|
||||||
|
return {html: gHtml, passN: passN, total: checks.length};
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
html += '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;background:var(--surface);border-radius:8px;border:0.5px solid var(--border);margin-bottom:6px;">';
|
||||||
|
html += '<div style="font-size:12px;color:var(--text)">当前 <strong style="color:var(--blue)">$'+Number(st.current_price).toFixed(0)+'</strong>';
|
||||||
|
if (zonePct !== null) html += ' <span style="color:var(--text3);margin:0 4px">·</span> '+zoneLabel+'('+Number(zonePct).toFixed(0)+'%)';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div style="display:flex;gap:4px;">';
|
||||||
|
if (distToRes !== null) html += '<span style="padding:2px 8px;border-radius:4px;font-size:11px;background:rgba(248,81,73,0.12);color:var(--red);font-weight:500">距阻力 $'+Number(distToRes).toFixed(0)+'</span>';
|
||||||
|
if (distToSup !== null) html += '<span style="padding:2px 8px;border-radius:4px;font-size:11px;background:rgba(63,185,80,0.08);color:var(--green)">距支撑 $'+Number(distToSup).toFixed(0)+'</span>';
|
||||||
|
html += '</div></div>';
|
||||||
|
|
||||||
|
// 做空条件组
|
||||||
|
const shortResult = renderCheckGroup(shortChecks, '↓ 做空条件(需全部通过)', 'var(--red)');
|
||||||
|
html += shortResult.html;
|
||||||
|
// 做多条件组
|
||||||
|
const longResult = renderCheckGroup(longChecks, '↑ 做多条件(需全部通过)', 'var(--green)');
|
||||||
|
html += longResult.html;
|
||||||
|
|
||||||
|
// D1 趋势状态提示
|
||||||
|
const d1Up = f['trend_up_1d'] ? f['trend_up_1d'].pass : false;
|
||||||
|
const d1Down = f['trend_down_1d'] ? f['trend_down_1d'].pass : false;
|
||||||
|
if (!d1Up && !d1Down) {
|
||||||
|
html += '<div style="padding:8px;margin-top:6px;background:rgba(210,153,34,0.1);border:0.5px solid var(--yellow);border-radius:6px;font-size:12px;color:var(--yellow);text-align:center">⚠ D1 趋势处于<strong>震荡</strong> — 做多做空总闸门均关闭,策略等待方向明确</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
// Also update diagnosis tab
|
||||||
|
const bar = document.getElementById('conclBar');
|
||||||
|
if (bar) {
|
||||||
|
const shortOk = shortResult.passN === shortResult.total;
|
||||||
|
const longOk = longResult.passN === longResult.total;
|
||||||
|
if (shortOk) { bar.className = 'cb rdy'; bar.textContent = '做空条件全部通过'; }
|
||||||
|
else if (longOk) { bar.className = 'cb rdy'; bar.textContent = '做多条件全部通过'; }
|
||||||
|
else { bar.className = 'cb no'; bar.textContent = '无方向满足 — 做空 '+shortResult.passN+'/5,做多 '+longResult.passN+'/5'; }
|
||||||
|
}
|
||||||
|
const fl = document.getElementById('fList');
|
||||||
|
if (fl) {
|
||||||
|
let diagHtml = '';
|
||||||
|
diagHtml += '<div class="sec-title">↑ 做多过滤</div>';
|
||||||
|
longChecks.forEach(c => {
|
||||||
|
const fl2 = f[c.key];
|
||||||
|
if (!fl2) return;
|
||||||
|
diagHtml += '<div class="fi"><div><div class="nm">'+c.label+'</div><div class="ds">'+fl2.desc+'</div></div><div class="st '+(fl2.pass?'st-p':'st-f')+'">'+(fl2.pass?'通过':'拒绝')+'</div></div>';
|
||||||
|
});
|
||||||
|
diagHtml += '<div class="sec-title" style="margin-top:8px">↓ 做空过滤</div>';
|
||||||
|
shortChecks.forEach(c => {
|
||||||
|
const fl2 = f[c.key];
|
||||||
|
if (!fl2) return;
|
||||||
|
diagHtml += '<div class="fi"><div><div class="nm">'+c.label+'</div><div class="ds">'+fl2.desc+'</div></div><div class="st '+(fl2.pass?'st-p':'st-f')+'">'+(fl2.pass?'通过':'拒绝')+'</div></div>';
|
||||||
|
});
|
||||||
|
fl.innerHTML = diagHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tradeInterval = null;
|
||||||
|
function swTab(n) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tc').forEach(t => t.classList.remove('active'));
|
||||||
|
const tabEl = document.querySelector(`.tab[onclick="swTab('${n}')"]`);
|
||||||
|
if (tabEl) tabEl.classList.add('active');
|
||||||
|
const contentEl = document.getElementById('tab-' + (n==='st'?'st':n==='diag'?'diag':'trade'));
|
||||||
|
if (contentEl) contentEl.classList.add('active');
|
||||||
|
if (n === 'trade') {
|
||||||
|
loadTradeData();
|
||||||
|
if (tradeInterval) clearInterval(tradeInterval);
|
||||||
|
tradeInterval = setInterval(loadTradeData, 30000);
|
||||||
|
} else {
|
||||||
|
if (tradeInterval) { clearInterval(tradeInterval); tradeInterval = null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function swSym(s) {
|
||||||
|
curSym = s;
|
||||||
|
document.querySelectorAll('.sb').forEach(b => b.classList.remove('ac'));
|
||||||
|
document.querySelector('.sb[onclick="swSym(\''+s+'\')"]').classList.add('ac');
|
||||||
|
if (cache[s]) render(); else loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() { loadData(); }
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 交易监测(Tab 3)
|
||||||
|
// ============================================================
|
||||||
|
let tradeCache = null;
|
||||||
|
|
||||||
|
async function loadTradeData() {
|
||||||
|
const headers = { 'X-API-Key': API_KEY, 'Accept': 'application/json' };
|
||||||
|
try {
|
||||||
|
const [accRes, tradeRes, statsRes] = await Promise.all([
|
||||||
|
fetch(SERVER_URL + '/api/account', { headers }),
|
||||||
|
fetch(SERVER_URL + '/api/trades?limit=50', { headers }),
|
||||||
|
fetch(SERVER_URL + '/api/trades/stats', { headers }),
|
||||||
|
]);
|
||||||
|
if (!accRes.ok || !tradeRes.ok || !statsRes.ok) throw new Error('API error');
|
||||||
|
tradeCache = {
|
||||||
|
account: await accRes.json(),
|
||||||
|
trades: await tradeRes.json(),
|
||||||
|
stats: await statsRes.json(),
|
||||||
|
};
|
||||||
|
renderTrade();
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('accGrid').innerHTML = '<div class="er" style="grid-column:1/-1">数据获取失败: ' + e.message + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTrade() {
|
||||||
|
if (!tradeCache) return;
|
||||||
|
const a = tradeCache.account.account;
|
||||||
|
const s = tradeCache.account.stats;
|
||||||
|
const open = tradeCache.account.open_positions || [];
|
||||||
|
const trades = tradeCache.trades.trades || [];
|
||||||
|
const daily = tradeCache.account.daily_pnl || [];
|
||||||
|
const dist = tradeCache.stats.profit_distribution || [];
|
||||||
|
|
||||||
|
document.getElementById('accBalance').textContent = '$' + (a.current_balance || 0).toFixed(2);
|
||||||
|
const pnlEl = document.getElementById('accPnl');
|
||||||
|
const pnl = a.total_pnl_pct || 0;
|
||||||
|
pnlEl.textContent = (pnl >= 0 ? '+' : '') + pnl.toFixed(2) + '%';
|
||||||
|
pnlEl.className = 'v ' + (pnl > 0 ? 'gn' : pnl < 0 ? 'rd' : '');
|
||||||
|
document.getElementById('accWinRate').textContent = (s.win_rate || 0) + '%';
|
||||||
|
document.getElementById('accTrades').textContent = s.total_trades || 0;
|
||||||
|
document.getElementById('accStrategy').textContent = '策略: ' + (a.strategy || '--');
|
||||||
|
document.getElementById('accUpdate').textContent = '更新: ' + new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});
|
||||||
|
|
||||||
|
let posHtml = '';
|
||||||
|
if (open.length === 0) {
|
||||||
|
posHtml = '<div class="empty-state">无持仓</div>';
|
||||||
|
} else {
|
||||||
|
open.forEach(p => {
|
||||||
|
const dir = p.is_short ? 'SHORT' : 'LONG';
|
||||||
|
const dirCls = p.is_short ? 'pos-short' : 'pos-long';
|
||||||
|
const pct = p.unrealized_pnl_pct ? (p.unrealized_pnl_pct * 100).toFixed(2) + '%' : '--';
|
||||||
|
const pctCls = p.unrealized_pnl_pct > 0 ? 'win' : p.unrealized_pnl_pct < 0 ? 'lose' : '';
|
||||||
|
posHtml += '<div class="tr">'
|
||||||
|
+ '<div class="tp">'
|
||||||
|
+ '<span><span class="pos-badge ' + dirCls + '">' + dir + '</span> ' + p.pair + '</span>'
|
||||||
|
+ '<span class="pnl ' + pctCls + '">' + pct + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="td">'
|
||||||
|
+ '<span>入场: $' + (p.open_rate || 0).toFixed(2) + '</span>'
|
||||||
|
+ '<span>数量: ' + (p.amount || 0).toFixed(4) + '</span>'
|
||||||
|
+ '<span>止损: $' + (p.stop_loss || 0).toFixed(2) + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('openPos').innerHTML = posHtml;
|
||||||
|
|
||||||
|
document.getElementById('tradeTotal').textContent = tradeCache.trades.pagination.total;
|
||||||
|
let tbody = '';
|
||||||
|
if (trades.length === 0) {
|
||||||
|
tbody = '<tr><td colspan="6" class="empty-state">暂无交易记录(策略刚开始运行或尚未开仓)</td></tr>';
|
||||||
|
} else {
|
||||||
|
trades.forEach(t => {
|
||||||
|
const dir = t.is_short ? '空' : '多';
|
||||||
|
const dirCls = t.is_short ? 'lose' : 'win';
|
||||||
|
const pnlPct = t.close_profit ? (t.close_profit * 100).toFixed(2) + '%' : '--';
|
||||||
|
const pnlCls = (t.close_profit || 0) > 0 ? 'win' : (t.close_profit || 0) < 0 ? 'lose' : '';
|
||||||
|
const openTime = t.open_date ? t.open_date.slice(5,16).replace('T',' ') : '--';
|
||||||
|
const exit = t.exit_reason || (t.is_open ? '持仓中' : '--');
|
||||||
|
tbody += '<tr>'
|
||||||
|
+ '<td>' + openTime + '</td>'
|
||||||
|
+ '<td class="' + dirCls + '">' + dir + '</td>'
|
||||||
|
+ '<td class="num">$' + (t.open_rate || 0).toFixed(2) + '</td>'
|
||||||
|
+ '<td class="num">' + (t.close_rate ? '$' + t.close_rate.toFixed(2) : '持仓中') + '</td>'
|
||||||
|
+ '<td class="num ' + pnlCls + '">' + pnlPct + '</td>'
|
||||||
|
+ '<td>' + exit + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('tradeBody').innerHTML = tbody;
|
||||||
|
|
||||||
|
renderDailyPnl(daily);
|
||||||
|
renderDistChart(dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDailyPnl(daily) {
|
||||||
|
const svg = document.getElementById('pnlSvg');
|
||||||
|
const w = 340, h = 120, pad = 20;
|
||||||
|
if (!daily || daily.length < 2) {
|
||||||
|
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/><text x="170" y="60" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rev = daily.slice().reverse();
|
||||||
|
const vals = rev.map(d => d.pnl_abs || 0);
|
||||||
|
const mn = Math.min(0, ...vals), mx = Math.max(0, ...vals);
|
||||||
|
const rg = (mx - mn) || 1;
|
||||||
|
const cw = w - pad*2, ch = h - pad*2 - 10;
|
||||||
|
const bw = rev.length > 1 ? Math.min(cw / rev.length, 20) : 10;
|
||||||
|
|
||||||
|
let bars = '';
|
||||||
|
const zero = pad + ch - ((0 - mn) / rg) * ch;
|
||||||
|
rev.forEach((d, i) => {
|
||||||
|
const x = pad + i * (cw / Math.max(rev.length - 1, 1));
|
||||||
|
const v = d.pnl_abs || 0;
|
||||||
|
const barH = Math.abs(v) / rg * ch;
|
||||||
|
const y = v >= 0 ? zero - barH : zero;
|
||||||
|
const color = v >= 0 ? 'var(--green)' : 'var(--red)';
|
||||||
|
bars += '<rect x="' + (x - bw/2) + '" y="' + y + '" width="' + bw + '" height="' + barH + '" fill="' + color + '" opacity="0.8" rx="1"/>';
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/>'
|
||||||
|
+ '<line x1="'+pad+'" y1="'+zero+'" x2="'+(w-pad)+'" y2="'+zero+'" stroke="var(--border)" stroke-width="0.5"/>'
|
||||||
|
+ bars
|
||||||
|
+ '<text x="'+pad+'" y="'+(h-4)+'" fill="var(--text3)" font-size="9">' + rev[0].day + '</text>'
|
||||||
|
+ '<text x="'+(w-pad)+'" y="'+(h-4)+'" text-anchor="end" fill="var(--text3)" font-size="9">' + rev[rev.length-1].day + '</text>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDistChart(dist) {
|
||||||
|
const svg = document.getElementById('distSvg');
|
||||||
|
const w = 340, h = 100, pad = 30;
|
||||||
|
if (!dist || dist.length === 0) {
|
||||||
|
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/><text x="170" y="50" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const maxCount = Math.max(...dist.map(d => d.count || 0), 1);
|
||||||
|
const cw = w - pad*2, ch = h - pad - 16;
|
||||||
|
const bw = cw / dist.length - 4;
|
||||||
|
|
||||||
|
let bars = '';
|
||||||
|
dist.forEach((d, i) => {
|
||||||
|
const x = pad + i * (cw / dist.length) + 2;
|
||||||
|
const barH = ((d.count || 0) / maxCount) * ch;
|
||||||
|
const y = h - 16 - barH;
|
||||||
|
const isLoss = d.profit_range && (d.profit_range.startsWith('\u2264') || d.profit_range.startsWith('-'));
|
||||||
|
const color = isLoss ? 'var(--red)' : 'var(--green)';
|
||||||
|
bars += '<rect x="' + x + '" y="' + y + '" width="' + bw + '" height="' + barH + '" fill="' + color + '" opacity="0.7" rx="1"/>';
|
||||||
|
bars += '<text x="' + (x + bw/2) + '" y="' + (h - 2) + '" text-anchor="middle" fill="var(--text3)" font-size="7" transform="rotate(-45,'+(x+bw/2)+','+(h-2)+')">' + (d.profit_range || '') + '</text>';
|
||||||
|
});
|
||||||
|
|
||||||
|
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/>' + bars;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (IS_CLOUDSTUDIO) {
|
||||||
|
document.getElementById('csBanner').style.display = 'block';
|
||||||
|
}
|
||||||
|
loadData();
|
||||||
|
setInterval(loadData, 60000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
399
dashboard/indicators.py
Normal file
399
dashboard/indicators.py
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
"""
|
||||||
|
指标计算模块 — 从 v2.2b 策略提取的独立版本
|
||||||
|
用于 APP 后端实时计算市场结构和信号诊断
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
def detect_swing_points(
|
||||||
|
high: pd.Series,
|
||||||
|
low: pd.Series,
|
||||||
|
window: int = 5,
|
||||||
|
) -> tuple[pd.Series, pd.Series]:
|
||||||
|
"""检测 Swing High / Swing Low,与 v2.2b 策略一致"""
|
||||||
|
n = len(high)
|
||||||
|
sh = pd.Series(np.nan, index=high.index, dtype=float)
|
||||||
|
sl = pd.Series(np.nan, index=low.index, dtype=float)
|
||||||
|
|
||||||
|
for i in range(window, n - window):
|
||||||
|
if high.iloc[i] > high.iloc[i - window : i].max() and high.iloc[i] > high.iloc[i + 1 : i + window + 1].max():
|
||||||
|
sh.iloc[i] = high.iloc[i]
|
||||||
|
if low.iloc[i] < low.iloc[i - window : i].min() and low.iloc[i] < low.iloc[i + 1 : i + window + 1].min():
|
||||||
|
sl.iloc[i] = low.iloc[i]
|
||||||
|
|
||||||
|
return sh, sl
|
||||||
|
|
||||||
|
|
||||||
|
def build_structure(
|
||||||
|
high: pd.Series,
|
||||||
|
low: pd.Series,
|
||||||
|
close: pd.Series,
|
||||||
|
swing_high: pd.Series,
|
||||||
|
swing_low: pd.Series,
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""构建价格结构(趋势方向、S/R 位、供需区),与 v2.2b 一致"""
|
||||||
|
n = len(high)
|
||||||
|
|
||||||
|
trend_up_arr = np.full(n, False)
|
||||||
|
trend_down_arr = np.full(n, False)
|
||||||
|
nearest_support = np.full(n, np.nan)
|
||||||
|
nearest_resistance = np.full(n, np.nan)
|
||||||
|
in_demand_zone = np.full(n, False)
|
||||||
|
in_supply_zone = np.full(n, False)
|
||||||
|
|
||||||
|
sh_prices = []
|
||||||
|
sl_prices = []
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
if pd.notna(swing_high.iloc[i]):
|
||||||
|
sh_prices.append(swing_high.iloc[i])
|
||||||
|
if len(sh_prices) > 4:
|
||||||
|
sh_prices.pop(0)
|
||||||
|
|
||||||
|
if pd.notna(swing_low.iloc[i]):
|
||||||
|
sl_prices.append(swing_low.iloc[i])
|
||||||
|
if len(sl_prices) > 4:
|
||||||
|
sl_prices.pop(0)
|
||||||
|
|
||||||
|
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
|
||||||
|
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
|
||||||
|
trend_up_arr[i] = True
|
||||||
|
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
|
||||||
|
trend_down_arr[i] = True
|
||||||
|
elif i > 0:
|
||||||
|
trend_up_arr[i] = trend_up_arr[i - 1]
|
||||||
|
trend_down_arr[i] = trend_down_arr[i - 1]
|
||||||
|
elif i > 0:
|
||||||
|
trend_up_arr[i] = trend_up_arr[i - 1]
|
||||||
|
trend_down_arr[i] = trend_down_arr[i - 1]
|
||||||
|
|
||||||
|
if sl_prices:
|
||||||
|
nearest_support[i] = sl_prices[-1]
|
||||||
|
if sh_prices:
|
||||||
|
nearest_resistance[i] = sh_prices[-1]
|
||||||
|
|
||||||
|
c = close.iloc[i]
|
||||||
|
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
|
||||||
|
zone_range = nearest_resistance[i] - nearest_support[i]
|
||||||
|
if zone_range > 0:
|
||||||
|
pos_pct = (c - nearest_support[i]) / zone_range
|
||||||
|
in_demand_zone[i] = pos_pct < 0.35
|
||||||
|
in_supply_zone[i] = pos_pct > 0.65
|
||||||
|
|
||||||
|
return pd.DataFrame({
|
||||||
|
"trend_up": trend_up_arr,
|
||||||
|
"trend_down": trend_down_arr,
|
||||||
|
"support": nearest_support,
|
||||||
|
"resistance": nearest_resistance,
|
||||||
|
"in_demand": in_demand_zone,
|
||||||
|
"in_supply": in_supply_zone,
|
||||||
|
}, index=high.index)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_trend_strength(
|
||||||
|
high: pd.Series,
|
||||||
|
low: pd.Series,
|
||||||
|
swing_high: pd.Series,
|
||||||
|
swing_low: pd.Series,
|
||||||
|
min_strength: float = -0.20,
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""计算趋势强度(4H Swing Point 间距变化),与 v2.2b 一致"""
|
||||||
|
sh_prices = []
|
||||||
|
sl_prices = []
|
||||||
|
trend_strength_up = np.full(len(high), np.nan)
|
||||||
|
trend_strength_down = np.full(len(high), np.nan)
|
||||||
|
strong_uptrend = np.full(len(high), False)
|
||||||
|
strong_downtrend = np.full(len(high), False)
|
||||||
|
|
||||||
|
for i in range(len(high)):
|
||||||
|
if pd.notna(swing_high.iloc[i]):
|
||||||
|
sh_prices.append(swing_high.iloc[i])
|
||||||
|
if len(sh_prices) > 4:
|
||||||
|
sh_prices.pop(0)
|
||||||
|
if pd.notna(swing_low.iloc[i]):
|
||||||
|
sl_prices.append(swing_low.iloc[i])
|
||||||
|
if len(sl_prices) > 4:
|
||||||
|
sl_prices.pop(0)
|
||||||
|
|
||||||
|
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
|
||||||
|
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
|
||||||
|
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
|
||||||
|
trend_strength_up[i] = hh_dist + hl_dist
|
||||||
|
trend_strength_down[i] = -(hh_dist + hl_dist)
|
||||||
|
|
||||||
|
strong_uptrend = pd.Series(trend_strength_up) > min_strength
|
||||||
|
strong_downtrend = pd.Series(trend_strength_down) > min_strength
|
||||||
|
|
||||||
|
return pd.DataFrame({
|
||||||
|
"trend_strength_up": pd.Series(trend_strength_up, index=high.index),
|
||||||
|
"trend_strength_down": pd.Series(trend_strength_down, index=high.index),
|
||||||
|
"strong_uptrend": strong_uptrend.values,
|
||||||
|
"strong_downtrend": strong_downtrend.values,
|
||||||
|
}, index=high.index)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_support_alive(
|
||||||
|
low: pd.Series,
|
||||||
|
close: pd.Series,
|
||||||
|
support: pd.Series,
|
||||||
|
) -> pd.Series:
|
||||||
|
"""检查支撑是否活着(最近测试过没跌破),与 v2.2b 一致"""
|
||||||
|
touched = (
|
||||||
|
(low <= support * 1.005) &
|
||||||
|
(low >= support * 0.995)
|
||||||
|
)
|
||||||
|
held = close > support
|
||||||
|
tested_and_held = touched & held
|
||||||
|
return tested_and_held.rolling(3, min_periods=1).max() > 0
|
||||||
|
|
||||||
|
|
||||||
|
def compute_resistance_alive(
|
||||||
|
high: pd.Series,
|
||||||
|
close: pd.Series,
|
||||||
|
resistance: pd.Series,
|
||||||
|
) -> pd.Series:
|
||||||
|
"""检查阻力是否活着(最近测试过没突破),与 v2.2b 一致"""
|
||||||
|
touched = (
|
||||||
|
(high >= resistance * 0.995) &
|
||||||
|
(high <= resistance * 1.005)
|
||||||
|
)
|
||||||
|
held = close < resistance
|
||||||
|
tested_and_held = touched & held
|
||||||
|
return tested_and_held.rolling(3, min_periods=1).max() > 0
|
||||||
|
|
||||||
|
|
||||||
|
def detect_candle_patterns(
|
||||||
|
open_: pd.Series,
|
||||||
|
high: pd.Series,
|
||||||
|
low: pd.Series,
|
||||||
|
close: pd.Series,
|
||||||
|
pin_bar_wick_ratio: float = 0.6,
|
||||||
|
) -> dict:
|
||||||
|
"""检测 K 线形态,与 v2.2b 一致"""
|
||||||
|
body = (close - open_).abs()
|
||||||
|
total_range = (high - low).replace(0, 0.0001)
|
||||||
|
|
||||||
|
upper_wick = high - close.where(close > open_, open_)
|
||||||
|
lower_wick = open_.where(close > open_, close) - low
|
||||||
|
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
|
||||||
|
|
||||||
|
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
|
||||||
|
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
|
||||||
|
|
||||||
|
prev_open = open_.shift(1)
|
||||||
|
prev_close = close.shift(1)
|
||||||
|
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
|
||||||
|
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bullish_pinbar": bullish_pin,
|
||||||
|
"bearish_pinbar": bearish_pin,
|
||||||
|
"bullish_signal": bullish_pin | bullish_engulf,
|
||||||
|
"bearish_signal": bearish_pin | bearish_engulf,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_all_indicators(
|
||||||
|
df_1h: pd.DataFrame,
|
||||||
|
df_4h: pd.DataFrame,
|
||||||
|
df_1d: pd.DataFrame,
|
||||||
|
params: dict = None,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
完整计算所有指标的入口函数
|
||||||
|
返回 APP 需要的全部数据
|
||||||
|
|
||||||
|
params 可选参数:
|
||||||
|
swing_lookback_d1: int (default 5)
|
||||||
|
swing_lookback_h4: int (default 8)
|
||||||
|
pin_bar_wick_ratio: float (default 0.6)
|
||||||
|
max_stop_dist: float (default 0.50 = 5%)
|
||||||
|
trend_strength_min: float (default -0.20)
|
||||||
|
"""
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
swing_lookback_d1 = params.get("swing_lookback_d1", 10) # v2.2d 策略默认 10
|
||||||
|
swing_lookback_h4 = params.get("swing_lookback_h4", 8)
|
||||||
|
swing_lookback_1h = params.get("swing_lookback_1h", 5)
|
||||||
|
pin_bar_wick_ratio = params.get("pin_bar_wick_ratio", 0.6)
|
||||||
|
max_stop_dist = params.get("max_stop_dist", 0.50)
|
||||||
|
trend_strength_min = params.get("trend_strength_min", -0.20)
|
||||||
|
|
||||||
|
# ---- D1 结构 ----
|
||||||
|
sh_d1, sl_d1 = detect_swing_points(df_1d["high"], df_1d["low"], swing_lookback_d1)
|
||||||
|
struct_d1 = build_structure(df_1d["high"], df_1d["low"], df_1d["close"], sh_d1, sl_d1)
|
||||||
|
trend_up_1d = bool(struct_d1["trend_up"].iloc[-1]) if len(struct_d1) > 0 else False
|
||||||
|
trend_down_1d = bool(struct_d1["trend_down"].iloc[-1]) if len(struct_d1) > 0 else False
|
||||||
|
|
||||||
|
# ---- D1 震荡检测(基于最近两个 swing 点,修复 build_structure 继承前值的误导) ----
|
||||||
|
sh_d1_valid = sh_d1.dropna()
|
||||||
|
sl_d1_valid = sl_d1.dropna()
|
||||||
|
if len(sh_d1_valid) >= 2 and len(sl_d1_valid) >= 2:
|
||||||
|
last_sh = sh_d1_valid.iloc[-1]
|
||||||
|
prev_sh = sh_d1_valid.iloc[-2]
|
||||||
|
last_sl = sl_d1_valid.iloc[-1]
|
||||||
|
prev_sl = sl_d1_valid.iloc[-2]
|
||||||
|
is_uptrend = last_sh > prev_sh and last_sl > prev_sl
|
||||||
|
is_downtrend = last_sh < prev_sh and last_sl < prev_sl
|
||||||
|
if not is_uptrend and not is_downtrend:
|
||||||
|
trend_up_1d = False
|
||||||
|
trend_down_1d = False
|
||||||
|
|
||||||
|
# ---- 4H 结构(仅趋势强度) ----
|
||||||
|
sh_4h, sl_4h = detect_swing_points(df_4h["high"], df_4h["low"], swing_lookback_h4)
|
||||||
|
struct_4h = build_structure(df_4h["high"], df_4h["low"], df_4h["close"], sh_4h, sl_4h)
|
||||||
|
strength_4h = compute_trend_strength(
|
||||||
|
df_4h["high"], df_4h["low"], sh_4h, sl_4h,
|
||||||
|
min_strength=trend_strength_min,
|
||||||
|
)
|
||||||
|
|
||||||
|
strong_uptrend_4h = bool(strength_4h["strong_uptrend"].iloc[-1]) if len(strength_4h) > 0 else False
|
||||||
|
strong_downtrend_4h = bool(strength_4h["strong_downtrend"].iloc[-1]) if len(strength_4h) > 0 else False
|
||||||
|
|
||||||
|
trend_strength_up_val = float(strength_4h["trend_strength_up"].iloc[-1]) if len(strength_4h) > 0 else 0
|
||||||
|
trend_strength_down_val = float(strength_4h["trend_strength_down"].iloc[-1]) if len(strength_4h) > 0 else 0
|
||||||
|
|
||||||
|
# ---- 1H 结构(S/R 适配 v2.2c) ----
|
||||||
|
sh_1h, sl_1h = detect_swing_points(df_1h["high"], df_1h["low"], swing_lookback_1h)
|
||||||
|
struct_1h = build_structure(df_1h["high"], df_1h["low"], df_1h["close"], sh_1h, sl_1h)
|
||||||
|
|
||||||
|
last_1h_s = struct_1h.iloc[-1] if len(struct_1h) > 0 else None
|
||||||
|
support_1h = float(last_1h_s["support"]) if last_1h_s is not None and pd.notna(last_1h_s["support"]) else None
|
||||||
|
resistance_1h = float(last_1h_s["resistance"]) if last_1h_s is not None and pd.notna(last_1h_s["resistance"]) else None
|
||||||
|
in_demand_1h = bool(last_1h_s["in_demand"]) if last_1h_s is not None else False
|
||||||
|
in_supply_1h = bool(last_1h_s["in_supply"]) if last_1h_s is not None else False
|
||||||
|
|
||||||
|
# 1H 活 S/R 检查
|
||||||
|
support_alive_1h = False
|
||||||
|
resistance_alive_1h = False
|
||||||
|
if support_1h is not None:
|
||||||
|
support_alive_1h = bool(compute_support_alive(df_1h["low"], df_1h["close"], struct_1h["support"]).iloc[-1])
|
||||||
|
if resistance_1h is not None:
|
||||||
|
resistance_alive_1h = bool(compute_resistance_alive(df_1h["high"], df_1h["close"], struct_1h["resistance"]).iloc[-1])
|
||||||
|
|
||||||
|
# ---- 1H K线形态 ----
|
||||||
|
last_1h_candle = df_1h.iloc[-1] if len(df_1h) > 0 else None
|
||||||
|
candle = detect_candle_patterns(
|
||||||
|
df_1h["open"], df_1h["high"], df_1h["low"], df_1h["close"],
|
||||||
|
pin_bar_wick_ratio,
|
||||||
|
)
|
||||||
|
bullish_signal = bool(candle["bullish_signal"].iloc[-1]) if len(df_1h) > 0 else False
|
||||||
|
bearish_signal = bool(candle["bearish_signal"].iloc[-1]) if len(df_1h) > 0 else False
|
||||||
|
bullish_pinbar = bool(candle["bullish_pinbar"].iloc[-1]) if len(df_1h) > 0 else False
|
||||||
|
bearish_pinbar = bool(candle["bearish_pinbar"].iloc[-1]) if len(df_1h) > 0 else False
|
||||||
|
|
||||||
|
# ---- 入场距离(1H S/R) ----
|
||||||
|
current_price = float(df_1h["open"].iloc[-1]) if len(df_1h) > 0 else 0
|
||||||
|
long_stop_dist = None
|
||||||
|
short_stop_dist = None
|
||||||
|
if support_1h is not None and support_1h > 0:
|
||||||
|
long_stop_dist = abs((current_price - support_1h) / current_price)
|
||||||
|
if resistance_1h is not None and resistance_1h > 0:
|
||||||
|
short_stop_dist = abs((resistance_1h - current_price) / current_price)
|
||||||
|
|
||||||
|
long_dist_ok = long_stop_dist is not None and long_stop_dist <= max_stop_dist and long_stop_dist > 0.003
|
||||||
|
short_dist_ok = short_stop_dist is not None and short_stop_dist <= max_stop_dist and short_stop_dist > 0.003
|
||||||
|
|
||||||
|
# ---- Swing Point 历史(1H 用于画图) ----
|
||||||
|
swing_highs_1h = df_1h[sh_1h.notna()].index.strftime("%Y-%m-%d %H:%M").tolist() if len(sh_1h) > 0 else []
|
||||||
|
swing_high_prices_1h = [float(x) for x in sh_1h.dropna().tolist()]
|
||||||
|
swing_lows_1h = df_1h[sl_1h.notna()].index.strftime("%Y-%m-%d %H:%M").tolist() if len(sl_1h) > 0 else []
|
||||||
|
swing_low_prices_1h = [float(x) for x in sl_1h.dropna().tolist()]
|
||||||
|
|
||||||
|
# ---- 信号诊断结果(1H S/R) ----
|
||||||
|
diagnosis = {
|
||||||
|
"market_price": round(current_price, 2),
|
||||||
|
"support": round(support_1h, 2) if support_1h else None,
|
||||||
|
"resistance": round(resistance_1h, 2) if resistance_1h else None,
|
||||||
|
"zone_width_pct": round(abs(resistance_1h - support_1h) / support_1h * 100, 2) if support_1h and resistance_1h else None,
|
||||||
|
"price_position_in_zone": round((current_price - support_1h) / (resistance_1h - support_1h) * 100, 1) if support_1h and resistance_1h and resistance_1h > support_1h else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"trend_up_1d": {
|
||||||
|
"pass": trend_up_1d,
|
||||||
|
"desc": "D1 上升趋势(HH+HL 都在抬高)",
|
||||||
|
"value": "上升" if trend_up_1d else ("下降" if trend_down_1d else "震荡"),
|
||||||
|
},
|
||||||
|
"in_demand_1h": {
|
||||||
|
"pass": in_demand_1h,
|
||||||
|
"desc": "价格在 1H 需求区(zone 底部 35%)",
|
||||||
|
"value": f"位置 {diagnosis['price_position_in_zone']}%" if diagnosis['price_position_in_zone'] is not None else "N/A",
|
||||||
|
},
|
||||||
|
"long_stop_distance": {
|
||||||
|
"pass": long_dist_ok,
|
||||||
|
"desc": f"入场距支撑距离 ({'%.2f' % (long_stop_dist*100) if long_stop_dist else 'N/A'}%)",
|
||||||
|
"value": f"{'%.2f' % (long_stop_dist*100)}%" if long_stop_dist else "N/A",
|
||||||
|
},
|
||||||
|
"support_alive_1h": {
|
||||||
|
"pass": support_alive_1h,
|
||||||
|
"desc": "1H 支撑最近被测试过且未跌破",
|
||||||
|
"value": "有效" if support_alive_1h else "失效/未测试",
|
||||||
|
},
|
||||||
|
"strong_uptrend_4h": {
|
||||||
|
"pass": strong_uptrend_4h,
|
||||||
|
"desc": "4H 趋势强度(扩张中)",
|
||||||
|
"value": f"{'%.2f' % (trend_strength_up_val*100)}%",
|
||||||
|
},
|
||||||
|
"trend_down_1d": {
|
||||||
|
"pass": trend_down_1d,
|
||||||
|
"desc": "D1 下降趋势(LH+LL 都在降低)",
|
||||||
|
"value": "下降" if trend_down_1d else ("上升" if trend_up_1d else "震荡"),
|
||||||
|
},
|
||||||
|
"in_supply_1h": {
|
||||||
|
"pass": in_supply_1h,
|
||||||
|
"desc": "价格在 1H 供给区(zone 顶部 65%+)",
|
||||||
|
"value": f"位置 {diagnosis['price_position_in_zone']}%" if diagnosis['price_position_in_zone'] is not None else "N/A",
|
||||||
|
},
|
||||||
|
"short_stop_distance": {
|
||||||
|
"pass": short_dist_ok,
|
||||||
|
"desc": f"入场距阻力距离 ({'%.2f' % (short_stop_dist*100) if short_stop_dist else 'N/A'}%)",
|
||||||
|
"value": f"{'%.2f' % (short_stop_dist*100)}%" if short_stop_dist else "N/A",
|
||||||
|
},
|
||||||
|
"resistance_alive_1h": {
|
||||||
|
"pass": resistance_alive_1h,
|
||||||
|
"desc": "1H 阻力最近被测试过且未突破",
|
||||||
|
"value": "有效" if resistance_alive_1h else "失效/未测试",
|
||||||
|
},
|
||||||
|
"strong_downtrend_4h": {
|
||||||
|
"pass": strong_downtrend_4h,
|
||||||
|
"desc": "4H 下降趋势强度(扩张中)",
|
||||||
|
"value": f"{'%.2f' % (trend_strength_down_val*100)}%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 入场可行性结论(1H S/R + 4H 趋势)
|
||||||
|
long_ok = all([
|
||||||
|
trend_up_1d, in_demand_1h, long_dist_ok,
|
||||||
|
support_alive_1h, strong_uptrend_4h,
|
||||||
|
])
|
||||||
|
short_ok = all([
|
||||||
|
trend_down_1d, in_supply_1h, short_dist_ok,
|
||||||
|
resistance_alive_1h, strong_downtrend_4h,
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": pd.Timestamp.now().isoformat(),
|
||||||
|
"current_price": round(current_price, 2),
|
||||||
|
"diagnosis": diagnosis,
|
||||||
|
"filters": filters,
|
||||||
|
"can_enter_long": long_ok,
|
||||||
|
"can_enter_short": short_ok,
|
||||||
|
"trend_1d": "up" if trend_up_1d else ("down" if trend_down_1d else "neutral"),
|
||||||
|
"candle_1h": {
|
||||||
|
"bullish_pinbar": bullish_pinbar,
|
||||||
|
"bearish_pinbar": bearish_pinbar,
|
||||||
|
"bullish_signal": bullish_signal,
|
||||||
|
"bearish_signal": bearish_signal,
|
||||||
|
},
|
||||||
|
"swing_points": {
|
||||||
|
"highs": [{"time": t, "price": p} for t, p in zip(swing_highs_1h[-10:], swing_high_prices_1h[-10:])],
|
||||||
|
"lows": [{"time": t, "price": p} for t, p in zip(swing_lows_1h[-10:], swing_low_prices_1h[-10:])],
|
||||||
|
},
|
||||||
|
"trend_strength_4h": {
|
||||||
|
"up": round(trend_strength_up_val * 100, 2),
|
||||||
|
"down": round(trend_strength_down_val * 100, 2),
|
||||||
|
},
|
||||||
|
}
|
||||||
499
dashboard/main.py
Normal file
499
dashboard/main.py
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
"""
|
||||||
|
Beast Trader API — 市场结构、信号诊断、交易监测数据
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import ccxt
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import FastAPI, HTTPException, Request, Query
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from indicators import compute_all_indicators
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 配置
|
||||||
|
# =============================================
|
||||||
|
API_KEY = os.environ.get("BEAST_API_KEY", "beast2025")
|
||||||
|
BINANCE_RATE_LIMIT_DELAY = 1.0 # 秒
|
||||||
|
DATA_CACHE_SECONDS = 30 # 缓存时间
|
||||||
|
|
||||||
|
# Freqtrade 数据库路径(只读)
|
||||||
|
FREQTRADE_DB = os.environ.get(
|
||||||
|
"FREQTRADE_DB",
|
||||||
|
os.path.expanduser("~/freqtrade/user_data/tradesv3.sqlite")
|
||||||
|
)
|
||||||
|
INITIAL_WALLET = float(os.environ.get("INITIAL_WALLET", 10000))
|
||||||
|
|
||||||
|
app = FastAPI(title="Beast Trader API", version="1.0")
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 缓存
|
||||||
|
# =============================================
|
||||||
|
_cache = {"data": None, "timestamp": 0}
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 认证中间件
|
||||||
|
# =============================================
|
||||||
|
@app.middleware("http")
|
||||||
|
async def auth_middleware(request: Request, call_next):
|
||||||
|
if request.method == "OPTIONS" or request.url.path in ["/api/health", "/", "/index.html"]:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
auth = request.headers.get("X-API-Key")
|
||||||
|
if not auth or auth != API_KEY:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 数据获取
|
||||||
|
# =============================================
|
||||||
|
def fetch_binance_data():
|
||||||
|
"""获取 BTC 和 ETH 的 D1/4H/1H K线数据 + 实时ticker"""
|
||||||
|
exchange = ccxt.binance({
|
||||||
|
"enableRateLimit": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
now = exchange.milliseconds()
|
||||||
|
since_1d = now - 180 * 24 * 60 * 60 * 1000 # 180天 D1
|
||||||
|
since_4h = now - 60 * 24 * 60 * 60 * 1000 # 60天 4H
|
||||||
|
since_1h = now - 14 * 24 * 60 * 60 * 1000 # 14天 1H
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for symbol in ["ETH/USDT", "BTC/USDT"]:
|
||||||
|
try:
|
||||||
|
ohlcv_1d = exchange.fetch_ohlcv(symbol, "1d", since_1d, 200)
|
||||||
|
ohlcv_4h = exchange.fetch_ohlcv(symbol, "4h", since_4h, 400)
|
||||||
|
ohlcv_1h = exchange.fetch_ohlcv(symbol, "1h", since_1h, 400)
|
||||||
|
ticker = exchange.fetch_ticker(symbol)
|
||||||
|
|
||||||
|
df_1d = pd.DataFrame(ohlcv_1d, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||||
|
df_1d["timestamp"] = pd.to_datetime(df_1d["timestamp"], unit="ms")
|
||||||
|
df_1d.set_index("timestamp", inplace=True)
|
||||||
|
|
||||||
|
df_4h = pd.DataFrame(ohlcv_4h, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||||
|
df_4h["timestamp"] = pd.to_datetime(df_4h["timestamp"], unit="ms")
|
||||||
|
df_4h.set_index("timestamp", inplace=True)
|
||||||
|
|
||||||
|
df_1h = pd.DataFrame(ohlcv_1h, columns=["timestamp", "open", "high", "low", "close", "volume"])
|
||||||
|
df_1h["timestamp"] = pd.to_datetime(df_1h["timestamp"], unit="ms")
|
||||||
|
df_1h.set_index("timestamp", inplace=True)
|
||||||
|
|
||||||
|
result[symbol] = {"1d": df_1d, "4h": df_4h, "1h": df_1h, "ticker": ticker}
|
||||||
|
time.sleep(0.5) # rate limit
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching {symbol}: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached_data():
|
||||||
|
"""获取缓存数据或刷新"""
|
||||||
|
now = time.time()
|
||||||
|
if now - _cache["timestamp"] < DATA_CACHE_SECONDS and _cache["data"] is not None:
|
||||||
|
return _cache["data"]
|
||||||
|
|
||||||
|
raw = fetch_binance_data()
|
||||||
|
result = {}
|
||||||
|
for symbol, frames in raw.items():
|
||||||
|
try:
|
||||||
|
computed = compute_all_indicators(frames["1h"], frames["4h"], frames["1d"])
|
||||||
|
# 用实时ticker覆盖current_price(指标里用的是1H open,不实时)
|
||||||
|
price = frames.get("ticker", {}).get("last")
|
||||||
|
if price is not None:
|
||||||
|
computed["current_price"] = round(float(price), 2)
|
||||||
|
result[symbol] = computed
|
||||||
|
# 附加最近K线数据用于图表
|
||||||
|
result[symbol]["price_history"] = {
|
||||||
|
"1h": {
|
||||||
|
"timestamps": frames["1h"].index[-48:].strftime("%m-%d %H:%M").tolist(),
|
||||||
|
"close": [round(float(x), 2) for x in frames["1h"]["close"].iloc[-48:].tolist()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error computing {symbol}: {e}")
|
||||||
|
result[symbol] = {"error": str(e)}
|
||||||
|
|
||||||
|
_cache["data"] = result
|
||||||
|
_cache["timestamp"] = now
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# API 路由
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok", "time": datetime.now(timezone.utc).isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/market-structure")
|
||||||
|
def market_structure():
|
||||||
|
"""
|
||||||
|
市场结构分析:S/R 位、Swing Point、趋势方向、供需区
|
||||||
|
"""
|
||||||
|
data = get_cached_data()
|
||||||
|
eth = data.get("ETH/USDT", {})
|
||||||
|
btc = data.get("BTC/USDT", {})
|
||||||
|
|
||||||
|
if "error" in eth:
|
||||||
|
raise HTTPException(status_code=500, detail=eth["error"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"eth": {
|
||||||
|
"current_price": eth.get("current_price"),
|
||||||
|
"support": eth.get("diagnosis", {}).get("support"),
|
||||||
|
"resistance": eth.get("diagnosis", {}).get("resistance"),
|
||||||
|
"zone_width_pct": eth.get("diagnosis", {}).get("zone_width_pct"),
|
||||||
|
"price_position_pct": eth.get("diagnosis", {}).get("price_position_in_zone"),
|
||||||
|
"trend_1d": eth.get("trend_1d"),
|
||||||
|
"trend_strength_4h": eth.get("trend_strength_4h"),
|
||||||
|
"swing_points": eth.get("swing_points"),
|
||||||
|
"price_history_1h": eth.get("price_history", {}).get("1h"),
|
||||||
|
},
|
||||||
|
"btc": {
|
||||||
|
"current_price": btc.get("current_price") if "error" not in btc else None,
|
||||||
|
"support_4h": btc.get("diagnosis", {}).get("support_4h"),
|
||||||
|
"resistance_4h": btc.get("diagnosis", {}).get("resistance_4h"),
|
||||||
|
"trend_1d": btc.get("trend_1d"),
|
||||||
|
},
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/signal-diagnosis")
|
||||||
|
def signal_diagnosis():
|
||||||
|
"""
|
||||||
|
信号诊断:8 层过滤条件逐一检查
|
||||||
|
"""
|
||||||
|
data = get_cached_data()
|
||||||
|
eth = data.get("ETH/USDT", {})
|
||||||
|
btc = data.get("BTC/USDT", {})
|
||||||
|
|
||||||
|
if "error" in eth:
|
||||||
|
raise HTTPException(status_code=500, detail=eth["error"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"eth": {
|
||||||
|
"can_enter_long": eth.get("can_enter_long"),
|
||||||
|
"can_enter_short": eth.get("can_enter_short"),
|
||||||
|
"diagnosis": eth.get("diagnosis"),
|
||||||
|
"filters": eth.get("filters"),
|
||||||
|
"candle_1h": eth.get("candle_1h"),
|
||||||
|
"trend_strength_4h": eth.get("trend_strength_4h"),
|
||||||
|
},
|
||||||
|
"btc": {
|
||||||
|
"can_enter_long": btc.get("can_enter_long"),
|
||||||
|
"can_enter_short": btc.get("can_enter_short"),
|
||||||
|
"diagnosis": btc.get("diagnosis"),
|
||||||
|
"filters": btc.get("filters"),
|
||||||
|
} if "error" not in btc else {"error": btc.get("error")},
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 静态文件服务(前端)
|
||||||
|
# =============================================
|
||||||
|
FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def serve_index():
|
||||||
|
return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 交易监测 API(读取 freqtrade SQLite 数据库)
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
def _get_db():
|
||||||
|
"""获取只读数据库连接"""
|
||||||
|
conn = sqlite3.connect(FREQTRADE_DB)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_dict(row):
|
||||||
|
"""将 sqlite3.Row 转为 dict,处理 datetime 和 boolean"""
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
d = {}
|
||||||
|
for k in row.keys():
|
||||||
|
v = row[k]
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
d[k] = v.decode("utf-8", errors="replace")
|
||||||
|
elif hasattr(v, "isoformat"):
|
||||||
|
d[k] = v.isoformat() if v else None
|
||||||
|
else:
|
||||||
|
d[k] = v
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/account")
|
||||||
|
def account_overview():
|
||||||
|
"""
|
||||||
|
账户概览:当前余额、盈亏统计、策略信息
|
||||||
|
数据来源:freqtrade SQLite 数据库 (wallet_history + trades)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = _get_db()
|
||||||
|
|
||||||
|
# 最新钱包余额
|
||||||
|
wallet = conn.execute(
|
||||||
|
"SELECT * FROM wallet_history ORDER BY timestamp DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
# 交易统计
|
||||||
|
stats = conn.execute("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_trades,
|
||||||
|
SUM(CASE WHEN is_open = 1 THEN 1 ELSE 0 END) as open_trades,
|
||||||
|
SUM(CASE WHEN is_open = 0 THEN 1 ELSE 0 END) as closed_trades,
|
||||||
|
SUM(CASE WHEN is_open = 0 AND close_profit > 0 THEN 1 ELSE 0 END) as winning_trades,
|
||||||
|
SUM(CASE WHEN is_open = 0 AND close_profit <= 0 THEN 1 ELSE 0 END) as losing_trades,
|
||||||
|
ROUND(SUM(close_profit_abs), 8) as total_profit_abs,
|
||||||
|
ROUND(AVG(CASE WHEN is_open = 0 THEN close_profit ELSE NULL END), 6) as avg_profit_pct,
|
||||||
|
ROUND(MAX(CASE WHEN is_open = 0 THEN close_profit ELSE NULL END), 6) as best_trade_pct,
|
||||||
|
ROUND(MIN(CASE WHEN is_open = 0 THEN close_profit ELSE NULL END), 6) as worst_trade_pct
|
||||||
|
FROM trades
|
||||||
|
""").fetchone()
|
||||||
|
|
||||||
|
# 当前持仓详情
|
||||||
|
open_positions = conn.execute("""
|
||||||
|
SELECT id, pair, is_short, open_rate, stake_amount, amount,
|
||||||
|
open_date, stop_loss, max_rate, min_rate,
|
||||||
|
ROUND((open_rate - ?) / open_rate * 100, 4) as unrealized_pnl_pct
|
||||||
|
FROM trades
|
||||||
|
WHERE is_open = 1
|
||||||
|
ORDER BY open_date DESC
|
||||||
|
""", [0]).fetchall() # placeholder, real current price handled in response
|
||||||
|
|
||||||
|
# 策略名称
|
||||||
|
strategy_row = conn.execute(
|
||||||
|
"SELECT DISTINCT strategy FROM trades WHERE strategy IS NOT NULL ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
strategy_name = strategy_row["strategy"] if strategy_row else "v2.2c"
|
||||||
|
|
||||||
|
# 近期每日盈亏(近30天)
|
||||||
|
daily_pnl = conn.execute("""
|
||||||
|
SELECT DATE(close_date) as day,
|
||||||
|
ROUND(SUM(close_profit_abs), 8) as pnl_abs,
|
||||||
|
COUNT(*) as trade_count,
|
||||||
|
SUM(CASE WHEN close_profit > 0 THEN 1 ELSE 0 END) as wins
|
||||||
|
FROM trades
|
||||||
|
WHERE is_open = 0 AND close_date IS NOT NULL
|
||||||
|
GROUP BY DATE(close_date)
|
||||||
|
ORDER BY day DESC
|
||||||
|
LIMIT 30
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 计算当前余额
|
||||||
|
if wallet:
|
||||||
|
current_balance = wallet["total_quote"] or wallet["balance"]
|
||||||
|
last_update = _row_to_dict(wallet)["timestamp"]
|
||||||
|
else:
|
||||||
|
current_balance = INITIAL_WALLET
|
||||||
|
last_update = None
|
||||||
|
|
||||||
|
# 计算总收益
|
||||||
|
total_pnl_pct = round((current_balance - INITIAL_WALLET) / INITIAL_WALLET * 100, 2) if current_balance else 0
|
||||||
|
|
||||||
|
# 胜率
|
||||||
|
closed = stats["closed_trades"] or 0
|
||||||
|
wins = stats["winning_trades"] or 0
|
||||||
|
win_rate = round(wins / closed * 100, 1) if closed > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"account": {
|
||||||
|
"initial_wallet": INITIAL_WALLET,
|
||||||
|
"current_balance": round(current_balance, 2),
|
||||||
|
"total_pnl_pct": total_pnl_pct,
|
||||||
|
"total_pnl_abs": round(current_balance - INITIAL_WALLET, 2),
|
||||||
|
"last_update": last_update,
|
||||||
|
"strategy": strategy_name,
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"total_trades": stats["total_trades"] or 0,
|
||||||
|
"open_trades": stats["open_trades"] or 0,
|
||||||
|
"closed_trades": closed,
|
||||||
|
"win_rate": win_rate,
|
||||||
|
"wins": wins,
|
||||||
|
"losses": stats["losing_trades"] or 0,
|
||||||
|
"avg_profit_pct": stats["avg_profit_pct"],
|
||||||
|
"best_trade_pct": stats["best_trade_pct"],
|
||||||
|
"worst_trade_pct": stats["worst_trade_pct"],
|
||||||
|
"total_profit_abs": stats["total_profit_abs"] or 0,
|
||||||
|
},
|
||||||
|
"open_positions": [_row_to_dict(r) for r in open_positions],
|
||||||
|
"daily_pnl": [_row_to_dict(r) for r in daily_pnl],
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/trades")
|
||||||
|
def trade_list(
|
||||||
|
limit: int = Query(default=30, ge=1, le=200),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
status: str = Query(default="all", pattern="^(all|open|closed)$"),
|
||||||
|
sort: str = Query(default="desc", pattern="^(asc|desc)$"),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
交易列表:支持分页和状态筛选
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = _get_db()
|
||||||
|
|
||||||
|
where_clause = ""
|
||||||
|
if status == "open":
|
||||||
|
where_clause = "WHERE is_open = 1"
|
||||||
|
elif status == "closed":
|
||||||
|
where_clause = "WHERE is_open = 0"
|
||||||
|
|
||||||
|
order = "DESC" if sort == "desc" else "ASC"
|
||||||
|
|
||||||
|
rows = conn.execute(f"""
|
||||||
|
SELECT id, pair, is_open, is_short, open_rate, close_rate,
|
||||||
|
close_profit, close_profit_abs, stake_amount, amount,
|
||||||
|
open_date, close_date, exit_reason, strategy,
|
||||||
|
max_rate, min_rate, stop_loss, initial_stop_loss,
|
||||||
|
enter_tag, leverage
|
||||||
|
FROM trades
|
||||||
|
{where_clause}
|
||||||
|
ORDER BY open_date {order}
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", [limit, offset]).fetchall()
|
||||||
|
|
||||||
|
total = conn.execute(f"SELECT COUNT(*) as cnt FROM trades {where_clause}").fetchone()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trades": [_row_to_dict(r) for r in rows],
|
||||||
|
"pagination": {
|
||||||
|
"total": total["cnt"],
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
"has_more": offset + limit < total["cnt"],
|
||||||
|
},
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/trades/stats")
|
||||||
|
def trade_stats():
|
||||||
|
"""
|
||||||
|
交易统计:按方向/时间/策略维度的聚合数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = _get_db()
|
||||||
|
|
||||||
|
# 按方向
|
||||||
|
by_direction = conn.execute("""
|
||||||
|
SELECT
|
||||||
|
CASE WHEN is_short = 1 THEN 'SHORT' ELSE 'LONG' END as direction,
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN is_open = 0 AND close_profit > 0 THEN 1 ELSE 0 END) as wins,
|
||||||
|
SUM(CASE WHEN is_open = 0 AND close_profit <= 0 THEN 1 ELSE 0 END) as losses,
|
||||||
|
ROUND(AVG(CASE WHEN is_open = 0 THEN close_profit ELSE NULL END), 6) as avg_profit_pct,
|
||||||
|
ROUND(SUM(CASE WHEN is_open = 0 THEN close_profit_abs ELSE 0 END), 8) as total_pnl_abs
|
||||||
|
FROM trades
|
||||||
|
WHERE is_open = 0
|
||||||
|
GROUP BY is_short
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
# 按退出原因
|
||||||
|
by_exit = conn.execute("""
|
||||||
|
SELECT
|
||||||
|
exit_reason,
|
||||||
|
COUNT(*) as count,
|
||||||
|
ROUND(AVG(close_profit), 6) as avg_profit_pct,
|
||||||
|
ROUND(SUM(close_profit_abs), 8) as total_pnl_abs
|
||||||
|
FROM trades
|
||||||
|
WHERE is_open = 0 AND exit_reason IS NOT NULL
|
||||||
|
GROUP BY exit_reason
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 10
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
# 月度表现
|
||||||
|
monthly = conn.execute("""
|
||||||
|
SELECT
|
||||||
|
strftime('%Y-%m', open_date) as month,
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN close_profit > 0 THEN 1 ELSE 0 END) as wins,
|
||||||
|
ROUND(AVG(close_profit), 6) as avg_profit_pct,
|
||||||
|
ROUND(SUM(close_profit_abs), 8) as total_pnl_abs
|
||||||
|
FROM trades
|
||||||
|
WHERE is_open = 0
|
||||||
|
GROUP BY strftime('%Y-%m', open_date)
|
||||||
|
ORDER BY month DESC
|
||||||
|
LIMIT 12
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
# 盈亏分布(区间统计)
|
||||||
|
distribution = conn.execute("""
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN close_profit <= -0.10 THEN '≤ -10%'
|
||||||
|
WHEN close_profit <= -0.05 THEN '-10% ~ -5%'
|
||||||
|
WHEN close_profit <= -0.02 THEN '-5% ~ -2%'
|
||||||
|
WHEN close_profit < 0 THEN '-2% ~ 0%'
|
||||||
|
WHEN close_profit = 0 THEN '0%'
|
||||||
|
WHEN close_profit <= 0.02 THEN '0% ~ 2%'
|
||||||
|
WHEN close_profit <= 0.05 THEN '2% ~ 5%'
|
||||||
|
WHEN close_profit <= 0.10 THEN '5% ~ 10%'
|
||||||
|
WHEN close_profit <= 0.20 THEN '10% ~ 20%'
|
||||||
|
ELSE '> 20%'
|
||||||
|
END as profit_range,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM trades
|
||||||
|
WHERE is_open = 0
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY MIN(close_profit)
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"by_direction": [_row_to_dict(r) for r in by_direction],
|
||||||
|
"by_exit_reason": [_row_to_dict(r) for r in by_exit],
|
||||||
|
"monthly": [_row_to_dict(r) for r in monthly],
|
||||||
|
"profit_distribution": [_row_to_dict(r) for r in distribution],
|
||||||
|
"cached_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 启动
|
||||||
|
# =============================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
port = int(os.environ.get("PORT", 8000))
|
||||||
|
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=False)
|
||||||
22
dashboard/manifest.json
Normal file
22
dashboard/manifest.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "Beast Trader",
|
||||||
|
"short_name": "BeastTrader",
|
||||||
|
"description": "ETH/USDT 结构分析 + 信号诊断",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0d1117",
|
||||||
|
"theme_color": "#0d1117",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
6
dashboard/requirements.txt
Normal file
6
dashboard/requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
ccxt
|
||||||
|
pandas
|
||||||
|
numpy
|
||||||
|
python-dotenv
|
||||||
247
dashboard/venv/bin/Activate.ps1
Normal file
247
dashboard/venv/bin/Activate.ps1
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
70
dashboard/venv/bin/activate
Normal file
70
dashboard/venv/bin/activate
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# You cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||||
|
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||||
|
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||||
|
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||||
|
export VIRTUAL_ENV=$(cygpath /home/ubuntu/trading_app/backend/venv)
|
||||||
|
else
|
||||||
|
# use the path as-is
|
||||||
|
export VIRTUAL_ENV=/home/ubuntu/trading_app/backend/venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1='(venv) '"${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT='(venv) '
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
27
dashboard/venv/bin/activate.csh
Normal file
27
dashboard/venv/bin/activate.csh
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV /home/ubuntu/trading_app/backend/venv
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = '(venv) '"$prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
69
dashboard/venv/bin/activate.fish
Normal file
69
dashboard/venv/bin/activate.fish
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/). You cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV /home/ubuntu/trading_app/backend/venv
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
end
|
||||||
8
dashboard/venv/bin/dotenv
Executable file
8
dashboard/venv/bin/dotenv
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dotenv.__main__ import cli
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli())
|
||||||
8
dashboard/venv/bin/f2py
Executable file
8
dashboard/venv/bin/f2py
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from numpy.f2py.f2py2e import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
dashboard/venv/bin/fastapi
Executable file
8
dashboard/venv/bin/fastapi
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from fastapi.cli import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
dashboard/venv/bin/idna
Executable file
8
dashboard/venv/bin/idna
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from idna.cli import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
dashboard/venv/bin/normalizer
Executable file
8
dashboard/venv/bin/normalizer
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from charset_normalizer.cli import cli_detect
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(cli_detect())
|
||||||
8
dashboard/venv/bin/numpy-config
Executable file
8
dashboard/venv/bin/numpy-config
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from numpy._configtool import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
dashboard/venv/bin/pip
Executable file
8
dashboard/venv/bin/pip
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
dashboard/venv/bin/pip3
Executable file
8
dashboard/venv/bin/pip3
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
dashboard/venv/bin/pip3.12
Executable file
8
dashboard/venv/bin/pip3.12
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
1
dashboard/venv/bin/python
Symbolic link
1
dashboard/venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
dashboard/venv/bin/python3
Symbolic link
1
dashboard/venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
1
dashboard/venv/bin/python3.12
Symbolic link
1
dashboard/venv/bin/python3.12
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
8
dashboard/venv/bin/uvicorn
Executable file
8
dashboard/venv/bin/uvicorn
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/home/ubuntu/trading_app/backend/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from uvicorn.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,239 @@
|
|||||||
|
# don't import any costly modules
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
report_url = (
|
||||||
|
"https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def warn_distutils_present():
|
||||||
|
if 'distutils' not in sys.modules:
|
||||||
|
return
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||||
|
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||||
|
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||||
|
"using distutils directly, ensure that setuptools is installed in the "
|
||||||
|
"traditional way (e.g. not an editable install), and/or make sure "
|
||||||
|
"that setuptools is always imported before distutils."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_distutils():
|
||||||
|
if 'distutils' not in sys.modules:
|
||||||
|
return
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Setuptools is replacing distutils. Support for replacing "
|
||||||
|
"an already imported distutils is deprecated. In the future, "
|
||||||
|
"this condition will fail. "
|
||||||
|
f"Register concerns at {report_url}"
|
||||||
|
)
|
||||||
|
mods = [
|
||||||
|
name
|
||||||
|
for name in sys.modules
|
||||||
|
if name == "distutils" or name.startswith("distutils.")
|
||||||
|
]
|
||||||
|
for name in mods:
|
||||||
|
del sys.modules[name]
|
||||||
|
|
||||||
|
|
||||||
|
def enabled():
|
||||||
|
"""
|
||||||
|
Allow selection of distutils by environment variable.
|
||||||
|
"""
|
||||||
|
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||||
|
if which == 'stdlib':
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Reliance on distutils from stdlib is deprecated. Users "
|
||||||
|
"must rely on setuptools to provide the distutils module. "
|
||||||
|
"Avoid importing distutils or import setuptools first, "
|
||||||
|
"and avoid setting SETUPTOOLS_USE_DISTUTILS=stdlib. "
|
||||||
|
f"Register concerns at {report_url}"
|
||||||
|
)
|
||||||
|
return which == 'local'
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_local_distutils():
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
clear_distutils()
|
||||||
|
|
||||||
|
# With the DistutilsMetaFinder in place,
|
||||||
|
# perform an import to cause distutils to be
|
||||||
|
# loaded from setuptools._distutils. Ref #2906.
|
||||||
|
with shim():
|
||||||
|
importlib.import_module('distutils')
|
||||||
|
|
||||||
|
# check that submodules load as expected
|
||||||
|
core = importlib.import_module('distutils.core')
|
||||||
|
assert '_distutils' in core.__file__, core.__file__
|
||||||
|
assert 'setuptools._distutils.log' not in sys.modules
|
||||||
|
|
||||||
|
|
||||||
|
def do_override():
|
||||||
|
"""
|
||||||
|
Ensure that the local copy of distutils is preferred over stdlib.
|
||||||
|
|
||||||
|
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||||
|
for more motivation.
|
||||||
|
"""
|
||||||
|
if enabled():
|
||||||
|
warn_distutils_present()
|
||||||
|
ensure_local_distutils()
|
||||||
|
|
||||||
|
|
||||||
|
class _TrivialRe:
|
||||||
|
def __init__(self, *patterns) -> None:
|
||||||
|
self._patterns = patterns
|
||||||
|
|
||||||
|
def match(self, string):
|
||||||
|
return all(pat in string for pat in self._patterns)
|
||||||
|
|
||||||
|
|
||||||
|
class DistutilsMetaFinder:
|
||||||
|
def find_spec(self, fullname, path, target=None):
|
||||||
|
# optimization: only consider top level modules and those
|
||||||
|
# found in the CPython test suite.
|
||||||
|
if path is not None and not fullname.startswith('test.'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||||
|
method = getattr(self, method_name, lambda: None)
|
||||||
|
return method()
|
||||||
|
|
||||||
|
def spec_for_distutils(self):
|
||||||
|
if self.is_cpython():
|
||||||
|
return None
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import importlib.abc
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module('setuptools._distutils')
|
||||||
|
except Exception:
|
||||||
|
# There are a couple of cases where setuptools._distutils
|
||||||
|
# may not be present:
|
||||||
|
# - An older Setuptools without a local distutils is
|
||||||
|
# taking precedence. Ref #2957.
|
||||||
|
# - Path manipulation during sitecustomize removes
|
||||||
|
# setuptools from the path but only after the hook
|
||||||
|
# has been loaded. Ref #2980.
|
||||||
|
# In either case, fall back to stdlib behavior.
|
||||||
|
return None
|
||||||
|
|
||||||
|
class DistutilsLoader(importlib.abc.Loader):
|
||||||
|
def create_module(self, spec):
|
||||||
|
mod.__name__ = 'distutils'
|
||||||
|
return mod
|
||||||
|
|
||||||
|
def exec_module(self, module):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return importlib.util.spec_from_loader(
|
||||||
|
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_cpython():
|
||||||
|
"""
|
||||||
|
Suppress supplying distutils for CPython (build and tests).
|
||||||
|
Ref #2965 and #3007.
|
||||||
|
"""
|
||||||
|
return os.path.isfile('pybuilddir.txt')
|
||||||
|
|
||||||
|
def spec_for_pip(self):
|
||||||
|
"""
|
||||||
|
Ensure stdlib distutils when running under pip.
|
||||||
|
See pypa/pip#8761 for rationale.
|
||||||
|
"""
|
||||||
|
if sys.version_info >= (3, 12) or self.pip_imported_during_build():
|
||||||
|
return
|
||||||
|
clear_distutils()
|
||||||
|
self.spec_for_distutils = lambda: None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def pip_imported_during_build(cls):
|
||||||
|
"""
|
||||||
|
Detect if pip is being imported in a build script. Ref #2355.
|
||||||
|
"""
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
return any(
|
||||||
|
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frame_file_is_setup(frame):
|
||||||
|
"""
|
||||||
|
Return True if the indicated frame suggests a setup.py file.
|
||||||
|
"""
|
||||||
|
# some frames may not have __file__ (#2940)
|
||||||
|
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||||
|
|
||||||
|
def spec_for_sensitive_tests(self):
|
||||||
|
"""
|
||||||
|
Ensure stdlib distutils when running select tests under CPython.
|
||||||
|
|
||||||
|
python/cpython#91169
|
||||||
|
"""
|
||||||
|
clear_distutils()
|
||||||
|
self.spec_for_distutils = lambda: None
|
||||||
|
|
||||||
|
sensitive_tests = (
|
||||||
|
[
|
||||||
|
'test.test_distutils',
|
||||||
|
'test.test_peg_generator',
|
||||||
|
'test.test_importlib',
|
||||||
|
]
|
||||||
|
if sys.version_info < (3, 10)
|
||||||
|
else [
|
||||||
|
'test.test_distutils',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for name in DistutilsMetaFinder.sensitive_tests:
|
||||||
|
setattr(
|
||||||
|
DistutilsMetaFinder,
|
||||||
|
f'spec_for_{name}',
|
||||||
|
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||||
|
|
||||||
|
|
||||||
|
def add_shim():
|
||||||
|
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||||
|
|
||||||
|
|
||||||
|
class shim:
|
||||||
|
def __enter__(self) -> None:
|
||||||
|
insert_shim()
|
||||||
|
|
||||||
|
def __exit__(self, exc: object, value: object, tb: object) -> None:
|
||||||
|
_remove_shim()
|
||||||
|
|
||||||
|
|
||||||
|
def insert_shim():
|
||||||
|
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_shim():
|
||||||
|
try:
|
||||||
|
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 12):
|
||||||
|
# DistutilsMetaFinder can only be disabled in Python < 3.12 (PEP 632)
|
||||||
|
remove_shim = _remove_shim
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
__import__('_distutils_hack').do_override()
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@ -0,0 +1,222 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: aiodns
|
||||||
|
Version: 4.0.4
|
||||||
|
Summary: Simple DNS resolver for asyncio
|
||||||
|
Author-email: Saúl Ibarra Corretgé <s@saghul.net>
|
||||||
|
License-Expression: MIT
|
||||||
|
Project-URL: repository, https://github.com/aio-libs/aiodns.git
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Requires-Python: >=3.10
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: pycares<6,>=5.0.0
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
===============================
|
||||||
|
Simple DNS resolver for asyncio
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. image:: https://badge.fury.io/py/aiodns.png
|
||||||
|
:target: https://pypi.org/project/aiodns/
|
||||||
|
|
||||||
|
.. image:: https://github.com/saghul/aiodns/workflows/CI/badge.svg
|
||||||
|
:target: https://github.com/saghul/aiodns/actions
|
||||||
|
|
||||||
|
aiodns provides a simple way for doing asynchronous DNS resolutions using `pycares <https://github.com/saghul/pycares>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Example
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiodns
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
resolver = aiodns.DNSResolver()
|
||||||
|
result = await resolver.query_dns('google.com', 'A')
|
||||||
|
for record in result.answer:
|
||||||
|
print(record.data.addr)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
The following query types are supported: A, AAAA, ANY, CAA, CNAME, MX, NAPTR, NS, PTR, SOA, SRV, TXT.
|
||||||
|
|
||||||
|
|
||||||
|
API
|
||||||
|
===
|
||||||
|
|
||||||
|
The API is pretty simple, the following functions are provided in the ``DNSResolver`` class:
|
||||||
|
|
||||||
|
* ``query_dns(host, type)``: Do a DNS resolution of the given type for the given hostname. It returns an
|
||||||
|
instance of ``asyncio.Future``. The result is a ``pycares.DNSResult`` object with ``answer``,
|
||||||
|
``authority``, and ``additional`` attributes containing lists of ``pycares.DNSRecord`` objects.
|
||||||
|
Each record has ``type``, ``ttl``, and ``data`` attributes. Check the `pycares documentation
|
||||||
|
<https://pycares.readthedocs.io/>`_ for details on the data attributes for each record type.
|
||||||
|
* ``query(host, type)``: **Deprecated** - use ``query_dns()`` instead. This method returns results
|
||||||
|
in a legacy format compatible with aiodns 3.x for backward compatibility.
|
||||||
|
* ``gethostbyname(host, socket_family)``: **Deprecated** - use ``getaddrinfo()`` instead.
|
||||||
|
Do a DNS resolution for the given hostname and the desired type of address family
|
||||||
|
(i.e. ``socket.AF_INET``). The actual result of the call is a ``asyncio.Future``.
|
||||||
|
* ``gethostbyaddr(name)``: Make a reverse lookup for an address.
|
||||||
|
* ``getaddrinfo(host, family, port, proto, type, flags)``: Resolve a host and port into a list of
|
||||||
|
address info entries.
|
||||||
|
* ``getnameinfo(sockaddr, flags)``: Resolve a socket address to a host and port.
|
||||||
|
* ``cancel()``: Cancel all pending DNS queries. All futures will get ``DNSError`` exception set, with
|
||||||
|
``ARES_ECANCELLED`` errno.
|
||||||
|
* ``close()``: Close the resolver. This releases all resources and cancels any pending queries. It must be called
|
||||||
|
when the resolver is no longer needed (e.g., application shutdown). The resolver should only be closed from the
|
||||||
|
event loop that created the resolver.
|
||||||
|
|
||||||
|
|
||||||
|
Migrating from aiodns 3.x
|
||||||
|
=========================
|
||||||
|
|
||||||
|
aiodns 4.x introduces a new ``query_dns()`` method that returns native pycares 5.x result types.
|
||||||
|
See the `pycares documentation <https://pycares.readthedocs.io/latest/channel.html#pycares.Channel.query>`_
|
||||||
|
for details on the result types. The old ``query()`` method is deprecated but continues to work
|
||||||
|
for backward compatibility.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# Old API (deprecated)
|
||||||
|
result = await resolver.query('example.com', 'MX')
|
||||||
|
for record in result:
|
||||||
|
print(record.host, record.priority)
|
||||||
|
|
||||||
|
# New API (recommended)
|
||||||
|
result = await resolver.query_dns('example.com', 'MX')
|
||||||
|
for record in result.answer:
|
||||||
|
print(record.data.exchange, record.data.priority)
|
||||||
|
|
||||||
|
|
||||||
|
Future migration to aiodns 5.x
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
The temporary ``query_dns()`` naming allows gradual migration without breaking changes:
|
||||||
|
|
||||||
|
+-----------+---------------------------------------+--------------------------------------------+
|
||||||
|
| Version | ``query()`` | ``query_dns()`` |
|
||||||
|
+===========+=======================================+============================================+
|
||||||
|
| **4.x** | Deprecated, returns compat types | New API, returns pycares 5.x types |
|
||||||
|
+-----------+---------------------------------------+--------------------------------------------+
|
||||||
|
| **5.x** | New API, returns pycares 5.x types | Alias to ``query()`` for back compat |
|
||||||
|
+-----------+---------------------------------------+--------------------------------------------+
|
||||||
|
|
||||||
|
In aiodns 5.x, ``query()`` will become the primary API returning native pycares 5.x types,
|
||||||
|
and ``query_dns()`` will remain as an alias for backward compatibility. This allows downstream
|
||||||
|
projects to migrate at their own pace.
|
||||||
|
|
||||||
|
|
||||||
|
Async Context Manager Support
|
||||||
|
=============================
|
||||||
|
|
||||||
|
While not recommended for typical use cases, ``DNSResolver`` can be used as an async context manager
|
||||||
|
for scenarios where automatic cleanup is desired:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
async with aiodns.DNSResolver() as resolver:
|
||||||
|
result = await resolver.query_dns('example.com', 'A')
|
||||||
|
# resolver.close() is called automatically when exiting the context
|
||||||
|
|
||||||
|
**Important**: This pattern is discouraged for most applications because ``DNSResolver`` instances
|
||||||
|
are designed to be long-lived and reused for many queries. Creating and destroying resolvers
|
||||||
|
frequently adds unnecessary overhead. Use the context manager pattern only when you specifically
|
||||||
|
need automatic cleanup for short-lived resolver instances, such as in tests or one-off scripts.
|
||||||
|
|
||||||
|
|
||||||
|
Note for Windows users
|
||||||
|
======================
|
||||||
|
|
||||||
|
This library requires the use of an ``asyncio.SelectorEventLoop`` or ``winloop`` on Windows
|
||||||
|
**only** when using a custom build of ``pycares`` that links against a system-
|
||||||
|
provided ``c-ares`` library **without** thread-safety support. This is because
|
||||||
|
non-thread-safe builds of ``c-ares`` are incompatible with the default
|
||||||
|
``ProactorEventLoop`` on Windows.
|
||||||
|
|
||||||
|
If you're using the official prebuilt ``pycares`` wheels on PyPI (version 4.7.0 or
|
||||||
|
later), which include a thread-safe version of ``c-ares``, this limitation does
|
||||||
|
**not** apply and can be safely ignored.
|
||||||
|
|
||||||
|
The default event loop can be changed as follows (do this very early in your application):
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||||
|
|
||||||
|
This may have other implications for the rest of your codebase, so make sure to test thoroughly.
|
||||||
|
|
||||||
|
|
||||||
|
Running the test suite
|
||||||
|
======================
|
||||||
|
|
||||||
|
To run the test suite: ``python -m pytest tests/``
|
||||||
|
|
||||||
|
|
||||||
|
Releasing (maintainers only)
|
||||||
|
============================
|
||||||
|
|
||||||
|
Releases are cut from ``master`` and published to PyPI automatically by the
|
||||||
|
``Release Wheels`` workflow when a GitHub Release is created.
|
||||||
|
|
||||||
|
1. **Prepare the release PR.** Bump ``__version__`` in ``aiodns/__init__.py``
|
||||||
|
and prepend a section to ``ChangeLog`` describing the user-facing changes
|
||||||
|
since the previous tag, in the same RST style as the existing entries
|
||||||
|
(``X.Y.Z`` header underlined with ``=``). Open the PR with the title
|
||||||
|
``Release X.Y.Z`` and merge it once CI is green.
|
||||||
|
|
||||||
|
Skip Dependabot bumps for dev tooling and CI actions; keep runtime
|
||||||
|
dependency bumps such as ``pycares``.
|
||||||
|
|
||||||
|
2. **Tag and publish the release.** From a clean checkout of ``master`` that
|
||||||
|
includes the merged release PR, generate the release notes from
|
||||||
|
``ChangeLog`` and create the GitHub release in one shot::
|
||||||
|
|
||||||
|
python scripts/release-notes.py --target X.Y.Z \
|
||||||
|
| gh release create vX.Y.Z --repo aio-libs/aiodns \
|
||||||
|
--title vX.Y.Z --notes-file -
|
||||||
|
|
||||||
|
The helper script reads ``__version__`` and the topmost ``ChangeLog``
|
||||||
|
section and aborts non-zero if they disagree, or if ``--target`` does
|
||||||
|
not match the current state on disk, so you can't accidentally publish
|
||||||
|
notes for a version the release PR hasn't actually landed yet.
|
||||||
|
|
||||||
|
3. **Watch the wheel build.** Publishing the GitHub release fires
|
||||||
|
``release-wheels.yml``, which builds wheels + sdist and pushes them to
|
||||||
|
`PyPI <https://pypi.org/project/aiodns/>`_ via trusted publishing
|
||||||
|
(no token required). Confirm the run succeeds::
|
||||||
|
|
||||||
|
gh run list --repo aio-libs/aiodns --workflow release-wheels.yml --limit 1
|
||||||
|
|
||||||
|
|
||||||
|
Author
|
||||||
|
======
|
||||||
|
|
||||||
|
Saúl Ibarra Corretgé <s@saghul.net>
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
aiodns uses the MIT license, check LICENSE file.
|
||||||
|
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
If you'd like to contribute, fork the project, make a patch and send a pull
|
||||||
|
request. Have a look at the surrounding code and please, make yours look
|
||||||
|
alike :-)
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
aiodns-4.0.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
aiodns-4.0.4.dist-info/METADATA,sha256=pVHVT-6YGLsPkHek3T52dJw-JihA1xholEOvymEPDLw,9022
|
||||||
|
aiodns-4.0.4.dist-info/RECORD,,
|
||||||
|
aiodns-4.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
||||||
|
aiodns-4.0.4.dist-info/licenses/LICENSE,sha256=PpHPp2ghq3DSKZgTbWdNIK6IhGOn6KfzdxZs_h7mT-s,1069
|
||||||
|
aiodns-4.0.4.dist-info/top_level.txt,sha256=5J2m3NWP4dezFZNpQoHaj8mv472iPcUzFboQoqWcEe4,7
|
||||||
|
aiodns/__init__.py,sha256=C8XPexYSfsN-sHf9hTmRMbGwgD189znOD-sduqQfkKQ,19381
|
||||||
|
aiodns/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiodns/__pycache__/compat.cpython-312.pyc,,
|
||||||
|
aiodns/__pycache__/error.cpython-312.pyc,,
|
||||||
|
aiodns/compat.py,sha256=lHsaYQ8ANvBpb6iqniMJoxuMXY_Cp-NmGRePvH4D91c,7941
|
||||||
|
aiodns/error.py,sha256=wYZFONch2rThb6sdMuKh_NUZBLJ5UgezHPwireqhQZE,1225
|
||||||
|
aiodns/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (82.0.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (C) 2014 by Saúl Ibarra Corretgé
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@ -0,0 +1 @@
|
|||||||
|
aiodns
|
||||||
550
dashboard/venv/lib/python3.12/site-packages/aiodns/__init__.py
Normal file
550
dashboard/venv/lib/python3.12/site-packages/aiodns/__init__.py
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
import weakref
|
||||||
|
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
|
||||||
|
|
||||||
|
import pycares
|
||||||
|
|
||||||
|
from . import error
|
||||||
|
from .compat import (
|
||||||
|
AresHostResult,
|
||||||
|
AresQueryAAAAResult,
|
||||||
|
AresQueryAResult,
|
||||||
|
AresQueryCAAResult,
|
||||||
|
AresQueryCNAMEResult,
|
||||||
|
AresQueryMXResult,
|
||||||
|
AresQueryNAPTRResult,
|
||||||
|
AresQueryNSResult,
|
||||||
|
AresQueryPTRResult,
|
||||||
|
AresQuerySOAResult,
|
||||||
|
AresQuerySRVResult,
|
||||||
|
AresQueryTXTResult,
|
||||||
|
QueryResult,
|
||||||
|
convert_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = '4.0.4'
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DNSResolver',
|
||||||
|
'error',
|
||||||
|
)
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
|
WINDOWS_SELECTOR_ERR_MSG = (
|
||||||
|
'aiodns needs a SelectorEventLoop on Windows. See more: '
|
||||||
|
'https://github.com/aio-libs/aiodns#note-for-windows-users'
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
query_type_map = {
|
||||||
|
'A': pycares.QUERY_TYPE_A,
|
||||||
|
'AAAA': pycares.QUERY_TYPE_AAAA,
|
||||||
|
'ANY': pycares.QUERY_TYPE_ANY,
|
||||||
|
'CAA': pycares.QUERY_TYPE_CAA,
|
||||||
|
'CNAME': pycares.QUERY_TYPE_CNAME,
|
||||||
|
'MX': pycares.QUERY_TYPE_MX,
|
||||||
|
'NAPTR': pycares.QUERY_TYPE_NAPTR,
|
||||||
|
'NS': pycares.QUERY_TYPE_NS,
|
||||||
|
'PTR': pycares.QUERY_TYPE_PTR,
|
||||||
|
'SOA': pycares.QUERY_TYPE_SOA,
|
||||||
|
'SRV': pycares.QUERY_TYPE_SRV,
|
||||||
|
'TXT': pycares.QUERY_TYPE_TXT,
|
||||||
|
}
|
||||||
|
|
||||||
|
query_class_map = {
|
||||||
|
'IN': pycares.QUERY_CLASS_IN,
|
||||||
|
'CHAOS': pycares.QUERY_CLASS_CHAOS,
|
||||||
|
'HS': pycares.QUERY_CLASS_HS,
|
||||||
|
'NONE': pycares.QUERY_CLASS_NONE,
|
||||||
|
'ANY': pycares.QUERY_CLASS_ANY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DNSResolver:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
nameservers: Sequence[str] | None = None,
|
||||||
|
loop: asyncio.AbstractEventLoop | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None: # TODO(PY311): Use Unpack for kwargs.
|
||||||
|
self._closed = True
|
||||||
|
self.loop = loop or asyncio.get_event_loop()
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert self.loop is not None
|
||||||
|
kwargs.pop('sock_state_cb', None)
|
||||||
|
timeout = kwargs.pop('timeout', None)
|
||||||
|
self._timeout = timeout
|
||||||
|
self._event_thread, self._channel = self._make_channel(**kwargs)
|
||||||
|
if nameservers:
|
||||||
|
self.nameservers = nameservers
|
||||||
|
self._read_fds: set[int] = set()
|
||||||
|
self._write_fds: set[int] = set()
|
||||||
|
self._timer: asyncio.TimerHandle | None = None
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def _make_channel(self, **kwargs: Any) -> tuple[bool, pycares.Channel]:
|
||||||
|
# pycares 5+ uses event_thread by default when sock_state_cb
|
||||||
|
# is not provided
|
||||||
|
try:
|
||||||
|
return True, pycares.Channel(timeout=self._timeout, **kwargs)
|
||||||
|
except pycares.AresError as e:
|
||||||
|
if sys.platform == 'linux':
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Failed to create DNS resolver channel with automatic '
|
||||||
|
'monitoring of resolver configuration changes. This '
|
||||||
|
'usually means the system ran out of inotify watches. '
|
||||||
|
'Falling back to socket state callback. Consider '
|
||||||
|
'increasing the system inotify watch limit: %s',
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Failed to create DNS resolver channel with automatic '
|
||||||
|
'monitoring of resolver configuration changes. '
|
||||||
|
'Falling back to socket state callback: %s',
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
# Fall back to sock_state_cb (needs SelectorEventLoop on Windows)
|
||||||
|
if sys.platform == 'win32' and not isinstance(
|
||||||
|
self.loop, asyncio.SelectorEventLoop
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
import winloop
|
||||||
|
|
||||||
|
if not isinstance(self.loop, winloop.Loop):
|
||||||
|
raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
|
||||||
|
except ModuleNotFoundError as ex:
|
||||||
|
raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG) from ex
|
||||||
|
# Use weak reference for deterministic cleanup. Without it there's a
|
||||||
|
# reference cycle (DNSResolver -> _channel -> callback -> DNSResolver).
|
||||||
|
# Python 3.4+ can handle cycles with __del__, but weak ref ensures
|
||||||
|
# cleanup happens immediately when last reference is dropped.
|
||||||
|
weak_self = weakref.ref(self)
|
||||||
|
|
||||||
|
def sock_state_cb_wrapper(
|
||||||
|
fd: int, readable: bool, writable: bool
|
||||||
|
) -> None:
|
||||||
|
this = weak_self()
|
||||||
|
if this is not None:
|
||||||
|
this._sock_state_cb(fd, readable, writable)
|
||||||
|
|
||||||
|
return False, pycares.Channel(
|
||||||
|
sock_state_cb=sock_state_cb_wrapper,
|
||||||
|
timeout=self._timeout,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nameservers(self) -> Sequence[str]:
|
||||||
|
# pycares 5.x returns servers with port (e.g., '8.8.8.8:53')
|
||||||
|
# Strip port for backward compatibility with pycares 4.x
|
||||||
|
return [s.rsplit(':', 1)[0] for s in self._channel.servers]
|
||||||
|
|
||||||
|
@nameservers.setter
|
||||||
|
def nameservers(self, value: Iterable[str | bytes]) -> None:
|
||||||
|
self._channel.servers = value
|
||||||
|
|
||||||
|
def _callback(
|
||||||
|
self, fut: asyncio.Future[_T], result: _T, errorno: int | None
|
||||||
|
) -> None:
|
||||||
|
# The future can already be done if pycares raised synchronously
|
||||||
|
# and _capture_ares_error set the exception before c-ares delivered
|
||||||
|
# the same error through this callback.
|
||||||
|
if fut.done():
|
||||||
|
return
|
||||||
|
if errorno is not None:
|
||||||
|
fut.set_exception(
|
||||||
|
error.DNSError(errorno, pycares.errno.strerror(errorno))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fut.set_result(result)
|
||||||
|
|
||||||
|
def _get_future_callback(
|
||||||
|
self,
|
||||||
|
) -> tuple[asyncio.Future[_T], Callable[[_T, int | None], None]]:
|
||||||
|
"""Return a future and a callback to set the result of the future."""
|
||||||
|
cb: Callable[[_T, int | None], None]
|
||||||
|
future: asyncio.Future[_T] = self.loop.create_future()
|
||||||
|
if self._event_thread:
|
||||||
|
cb = functools.partial( # type: ignore[assignment]
|
||||||
|
self.loop.call_soon_threadsafe,
|
||||||
|
self._callback, # type: ignore[arg-type]
|
||||||
|
future,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cb = functools.partial(self._callback, future)
|
||||||
|
return future, cb
|
||||||
|
|
||||||
|
def _query_callback(
|
||||||
|
self,
|
||||||
|
fut: asyncio.Future[QueryResult],
|
||||||
|
qtype: int,
|
||||||
|
result: pycares.DNSResult,
|
||||||
|
errorno: int | None,
|
||||||
|
) -> None:
|
||||||
|
"""Callback for query that converts results to compatible format."""
|
||||||
|
# See _callback for why we guard on done() rather than cancelled().
|
||||||
|
if fut.done():
|
||||||
|
return
|
||||||
|
if errorno is not None:
|
||||||
|
fut.set_exception(
|
||||||
|
error.DNSError(errorno, pycares.errno.strerror(errorno))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
converted = convert_result(result, qtype)
|
||||||
|
except error.DNSError as exc:
|
||||||
|
fut.set_exception(exc)
|
||||||
|
else:
|
||||||
|
fut.set_result(converted)
|
||||||
|
|
||||||
|
def _get_query_future_callback(
|
||||||
|
self, qtype: int
|
||||||
|
) -> tuple[asyncio.Future[QueryResult], Callable[..., None]]:
|
||||||
|
"""Return a future and callback for query with result conversion."""
|
||||||
|
future: asyncio.Future[QueryResult] = self.loop.create_future()
|
||||||
|
cb: Callable[..., None]
|
||||||
|
if self._event_thread:
|
||||||
|
cb = functools.partial( # type: ignore[assignment]
|
||||||
|
self.loop.call_soon_threadsafe,
|
||||||
|
self._query_callback, # type: ignore[arg-type]
|
||||||
|
future,
|
||||||
|
qtype,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cb = functools.partial(self._query_callback, future, qtype)
|
||||||
|
return future, cb
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _capture_ares_error(self, fut: asyncio.Future[_T]) -> Iterator[None]:
|
||||||
|
# When pycares raises synchronously (e.g. ARES_EBADNAME for a
|
||||||
|
# malformed hostname), c-ares may also invoke the callback first,
|
||||||
|
# leaving the future already done. Route the error through the
|
||||||
|
# future so callers can rely on `await` to raise.
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except pycares.AresError as exc:
|
||||||
|
if fut.done():
|
||||||
|
return
|
||||||
|
# pycares always raises (errno, message), but be defensive:
|
||||||
|
# an args-less AresError should still resolve the future to
|
||||||
|
# avoid an indefinite hang on `await`.
|
||||||
|
errno = exc.args[0] if exc.args else error.ARES_EFORMERR
|
||||||
|
fut.set_exception(
|
||||||
|
error.DNSError(errno, pycares.errno.strerror(errno))
|
||||||
|
)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['A'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryAResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['AAAA'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryAAAAResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['CAA'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryCAAResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['CNAME'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[AresQueryCNAMEResult]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['MX'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryMXResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['NAPTR'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryNAPTRResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['NS'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryNSResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['PTR'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[AresQueryPTRResult]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['SOA'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[AresQuerySOAResult]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['SRV'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQuerySRVResult]]: ...
|
||||||
|
@overload
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: Literal['TXT'], qclass: str | None = ...
|
||||||
|
) -> asyncio.Future[list[AresQueryTXTResult]]: ...
|
||||||
|
|
||||||
|
def query(
|
||||||
|
self, host: str, qtype: str, qclass: str | None = None
|
||||||
|
) -> asyncio.Future[list[Any]] | asyncio.Future[Any]:
|
||||||
|
"""Query DNS records (deprecated, use query_dns instead)."""
|
||||||
|
warnings.warn(
|
||||||
|
'query() is deprecated, use query_dns() instead',
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
qtype_int = query_type_map[qtype]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f'invalid query type: {qtype}') from e
|
||||||
|
qclass_int: int | None = None
|
||||||
|
if qclass is not None:
|
||||||
|
try:
|
||||||
|
qclass_int = query_class_map[qclass]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f'invalid query class: {qclass}') from e
|
||||||
|
|
||||||
|
fut, cb = self._get_query_future_callback(qtype_int)
|
||||||
|
with self._capture_ares_error(fut):
|
||||||
|
if qclass_int is not None:
|
||||||
|
self._channel.query(
|
||||||
|
host, qtype_int, query_class=qclass_int, callback=cb
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._channel.query(host, qtype_int, callback=cb)
|
||||||
|
return fut
|
||||||
|
|
||||||
|
def query_dns(
|
||||||
|
self, host: str, qtype: str, qclass: str | None = None
|
||||||
|
) -> asyncio.Future[pycares.DNSResult]:
|
||||||
|
"""Query DNS records, returning native pycares 5.x DNSResult."""
|
||||||
|
try:
|
||||||
|
qtype_int = query_type_map[qtype]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f'invalid query type: {qtype}') from e
|
||||||
|
qclass_int: int | None = None
|
||||||
|
if qclass is not None:
|
||||||
|
try:
|
||||||
|
qclass_int = query_class_map[qclass]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(f'invalid query class: {qclass}') from e
|
||||||
|
|
||||||
|
fut: asyncio.Future[pycares.DNSResult]
|
||||||
|
fut, cb = self._get_future_callback()
|
||||||
|
with self._capture_ares_error(fut):
|
||||||
|
if qclass_int is not None:
|
||||||
|
self._channel.query(
|
||||||
|
host, qtype_int, query_class=qclass_int, callback=cb
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._channel.query(host, qtype_int, callback=cb)
|
||||||
|
return fut
|
||||||
|
|
||||||
|
def _gethostbyname_callback(
|
||||||
|
self,
|
||||||
|
fut: asyncio.Future[AresHostResult],
|
||||||
|
host: str,
|
||||||
|
result: pycares.AddrInfoResult | None,
|
||||||
|
errorno: int | None,
|
||||||
|
) -> None:
|
||||||
|
"""Callback for gethostbyname that converts AddrInfoResult."""
|
||||||
|
# See _callback for why we guard on done() rather than cancelled().
|
||||||
|
if fut.done():
|
||||||
|
return
|
||||||
|
if errorno is not None:
|
||||||
|
fut.set_exception(
|
||||||
|
error.DNSError(errorno, pycares.errno.strerror(errorno))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert result is not None # noqa: S101
|
||||||
|
# node.addr is (address_bytes, port) - extract and decode
|
||||||
|
addresses = [node.addr[0].decode() for node in result.nodes]
|
||||||
|
# Get canonical name from cnames if available
|
||||||
|
name = result.cnames[0].name if result.cnames else host
|
||||||
|
fut.set_result(
|
||||||
|
AresHostResult(name=name, aliases=[], addresses=addresses)
|
||||||
|
)
|
||||||
|
|
||||||
|
def gethostbyname(
|
||||||
|
self, host: str, family: socket.AddressFamily
|
||||||
|
) -> asyncio.Future[AresHostResult]:
|
||||||
|
"""
|
||||||
|
Resolve hostname to addresses.
|
||||||
|
|
||||||
|
Deprecated: Use getaddrinfo() instead. This is implemented using
|
||||||
|
getaddrinfo as pycares 5.x removed the gethostbyname method.
|
||||||
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
'gethostbyname() is deprecated, use getaddrinfo() instead',
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
fut: asyncio.Future[AresHostResult] = self.loop.create_future()
|
||||||
|
cb: Callable[..., None]
|
||||||
|
if self._event_thread:
|
||||||
|
cb = functools.partial( # type: ignore[assignment]
|
||||||
|
self.loop.call_soon_threadsafe,
|
||||||
|
self._gethostbyname_callback, # type: ignore[arg-type]
|
||||||
|
fut,
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cb = functools.partial(self._gethostbyname_callback, fut, host)
|
||||||
|
with self._capture_ares_error(fut):
|
||||||
|
self._channel.getaddrinfo(host, None, family=family, callback=cb)
|
||||||
|
return fut
|
||||||
|
|
||||||
|
def getaddrinfo(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
family: socket.AddressFamily = socket.AF_UNSPEC,
|
||||||
|
port: int | None = None,
|
||||||
|
proto: int = 0,
|
||||||
|
type: int = 0,
|
||||||
|
flags: int = 0,
|
||||||
|
) -> asyncio.Future[pycares.AddrInfoResult]:
|
||||||
|
fut: asyncio.Future[pycares.AddrInfoResult]
|
||||||
|
fut, cb = self._get_future_callback()
|
||||||
|
with self._capture_ares_error(fut):
|
||||||
|
self._channel.getaddrinfo(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
family=family,
|
||||||
|
type=type,
|
||||||
|
proto=proto,
|
||||||
|
flags=flags,
|
||||||
|
callback=cb,
|
||||||
|
)
|
||||||
|
return fut
|
||||||
|
|
||||||
|
def getnameinfo(
|
||||||
|
self,
|
||||||
|
sockaddr: tuple[str, int] | tuple[str, int, int, int],
|
||||||
|
flags: int = 0,
|
||||||
|
) -> asyncio.Future[pycares.NameInfoResult]:
|
||||||
|
fut: asyncio.Future[pycares.NameInfoResult]
|
||||||
|
fut, cb = self._get_future_callback()
|
||||||
|
with self._capture_ares_error(fut):
|
||||||
|
self._channel.getnameinfo(sockaddr, flags, callback=cb)
|
||||||
|
return fut
|
||||||
|
|
||||||
|
def gethostbyaddr(self, name: str) -> asyncio.Future[pycares.HostResult]:
|
||||||
|
fut: asyncio.Future[pycares.HostResult]
|
||||||
|
fut, cb = self._get_future_callback()
|
||||||
|
with self._capture_ares_error(fut):
|
||||||
|
self._channel.gethostbyaddr(name, callback=cb)
|
||||||
|
return fut
|
||||||
|
|
||||||
|
def cancel(self) -> None:
|
||||||
|
self._channel.cancel()
|
||||||
|
|
||||||
|
def _sock_state_cb(self, fd: int, readable: bool, writable: bool) -> None:
|
||||||
|
if readable or writable:
|
||||||
|
if readable:
|
||||||
|
self.loop.add_reader(
|
||||||
|
fd, self._channel.process_fd, fd, pycares.ARES_SOCKET_BAD
|
||||||
|
)
|
||||||
|
self._read_fds.add(fd)
|
||||||
|
if writable:
|
||||||
|
self.loop.add_writer(
|
||||||
|
fd, self._channel.process_fd, pycares.ARES_SOCKET_BAD, fd
|
||||||
|
)
|
||||||
|
self._write_fds.add(fd)
|
||||||
|
if self._timer is None:
|
||||||
|
self._start_timer()
|
||||||
|
else:
|
||||||
|
# socket is now closed
|
||||||
|
if fd in self._read_fds:
|
||||||
|
self._read_fds.discard(fd)
|
||||||
|
self.loop.remove_reader(fd)
|
||||||
|
|
||||||
|
if fd in self._write_fds:
|
||||||
|
self._write_fds.discard(fd)
|
||||||
|
self.loop.remove_writer(fd)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not self._read_fds
|
||||||
|
and not self._write_fds
|
||||||
|
and self._timer is not None
|
||||||
|
):
|
||||||
|
self._timer.cancel()
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
def _timer_cb(self) -> None:
|
||||||
|
if self._read_fds or self._write_fds:
|
||||||
|
self._channel.process_fd(
|
||||||
|
pycares.ARES_SOCKET_BAD, pycares.ARES_SOCKET_BAD
|
||||||
|
)
|
||||||
|
self._start_timer()
|
||||||
|
else:
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
def _start_timer(self) -> None:
|
||||||
|
timeout = self._timeout
|
||||||
|
if timeout is None or timeout < 0 or timeout > 1:
|
||||||
|
timeout = 1
|
||||||
|
elif timeout == 0:
|
||||||
|
timeout = 0.1
|
||||||
|
|
||||||
|
self._timer = self.loop.call_later(timeout, self._timer_cb)
|
||||||
|
|
||||||
|
def _cleanup(self) -> None:
|
||||||
|
"""Cleanup timers and file descriptors when closing resolver."""
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
# Mark as closed first to prevent double cleanup
|
||||||
|
self._closed = True
|
||||||
|
# Cancel timer if running
|
||||||
|
if self._timer is not None:
|
||||||
|
self._timer.cancel()
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
# Remove all file descriptors
|
||||||
|
for fd in self._read_fds:
|
||||||
|
self.loop.remove_reader(fd)
|
||||||
|
for fd in self._write_fds:
|
||||||
|
self.loop.remove_writer(fd)
|
||||||
|
|
||||||
|
self._read_fds.clear()
|
||||||
|
self._write_fds.clear()
|
||||||
|
self._channel.close()
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""
|
||||||
|
Cleanly close the DNS resolver.
|
||||||
|
|
||||||
|
This should be called to ensure all resources are properly released.
|
||||||
|
After calling close(), the resolver should not be used again.
|
||||||
|
"""
|
||||||
|
if not self._closed:
|
||||||
|
self._channel.cancel()
|
||||||
|
self._cleanup()
|
||||||
|
|
||||||
|
async def __aenter__(self) -> DNSResolver:
|
||||||
|
"""Enter the async context manager."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(
|
||||||
|
self,
|
||||||
|
exc_type: type[BaseException] | None,
|
||||||
|
exc_val: BaseException | None,
|
||||||
|
exc_tb: TracebackType | None,
|
||||||
|
) -> None:
|
||||||
|
"""Exit the async context manager."""
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
"""Handle cleanup when the resolver is garbage collected."""
|
||||||
|
# Check if we have a channel to clean up
|
||||||
|
# This can happen if an exception occurs during __init__ before
|
||||||
|
# _channel is created (e.g., RuntimeError on Windows
|
||||||
|
# without proper loop)
|
||||||
|
if hasattr(self, '_channel'):
|
||||||
|
self._cleanup()
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
288
dashboard/venv/lib/python3.12/site-packages/aiodns/compat.py
Normal file
288
dashboard/venv/lib/python3.12/site-packages/aiodns/compat.py
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
"""
|
||||||
|
Compatibility layer for pycares 5.x API.
|
||||||
|
|
||||||
|
This module provides result types compatible with pycares 4.x API
|
||||||
|
to maintain backward compatibility with existing code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Union, cast
|
||||||
|
|
||||||
|
import pycares
|
||||||
|
|
||||||
|
from . import error
|
||||||
|
|
||||||
|
_SINGLE_RESULT_QTYPES = frozenset(
|
||||||
|
{
|
||||||
|
pycares.QUERY_TYPE_CNAME,
|
||||||
|
pycares.QUERY_TYPE_SOA,
|
||||||
|
pycares.QUERY_TYPE_PTR,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_str(data: bytes) -> str | bytes:
|
||||||
|
"""Decode bytes as ASCII, return bytes if decode fails (pycares 4.x)."""
|
||||||
|
try:
|
||||||
|
return data.decode('ascii')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryAResult:
|
||||||
|
"""A record result (compatible with pycares 4.x ares_query_a_result)."""
|
||||||
|
|
||||||
|
host: str
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryAAAAResult:
|
||||||
|
"""AAAA record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
host: str
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryCNAMEResult:
|
||||||
|
"""CNAME record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
cname: str
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryMXResult:
|
||||||
|
"""MX record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
host: str
|
||||||
|
priority: int
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryNSResult:
|
||||||
|
"""NS record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
host: str
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryTXTResult:
|
||||||
|
"""TXT record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
text: str | bytes # str if ASCII, bytes otherwise (pycares 4.x behavior)
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQuerySOAResult:
|
||||||
|
"""SOA record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
nsname: str
|
||||||
|
hostmaster: str
|
||||||
|
serial: int
|
||||||
|
refresh: int
|
||||||
|
retry: int
|
||||||
|
expires: int
|
||||||
|
minttl: int
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQuerySRVResult:
|
||||||
|
"""SRV record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
priority: int
|
||||||
|
weight: int
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryNAPTRResult:
|
||||||
|
"""NAPTR record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
order: int
|
||||||
|
preference: int
|
||||||
|
flags: str
|
||||||
|
service: str
|
||||||
|
regex: str
|
||||||
|
replacement: str
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryCAAResult:
|
||||||
|
"""CAA record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
critical: int
|
||||||
|
property: str
|
||||||
|
value: str
|
||||||
|
ttl: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresQueryPTRResult:
|
||||||
|
"""PTR record result (pycares 4.x compat)."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
ttl: int
|
||||||
|
aliases: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AresHostResult:
|
||||||
|
"""Host result (compatible with pycares 4.x ares_host_result)."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
aliases: list[str]
|
||||||
|
addresses: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
# Type alias for a single converted record
|
||||||
|
ConvertedRecord = Union[
|
||||||
|
AresQueryAResult,
|
||||||
|
AresQueryAAAAResult,
|
||||||
|
AresQueryCNAMEResult,
|
||||||
|
AresQueryMXResult,
|
||||||
|
AresQueryNSResult,
|
||||||
|
AresQueryTXTResult,
|
||||||
|
AresQuerySOAResult,
|
||||||
|
AresQuerySRVResult,
|
||||||
|
AresQueryNAPTRResult,
|
||||||
|
AresQueryCAAResult,
|
||||||
|
AresQueryPTRResult,
|
||||||
|
pycares.DNSRecord, # Unknown types returned as-is
|
||||||
|
]
|
||||||
|
|
||||||
|
# Type alias for query results
|
||||||
|
QueryResult = Union[
|
||||||
|
list[AresQueryAResult],
|
||||||
|
list[AresQueryAAAAResult],
|
||||||
|
AresQueryCNAMEResult,
|
||||||
|
list[AresQueryMXResult],
|
||||||
|
list[AresQueryNSResult],
|
||||||
|
list[AresQueryTXTResult],
|
||||||
|
AresQuerySOAResult,
|
||||||
|
list[AresQuerySRVResult],
|
||||||
|
list[AresQueryNAPTRResult],
|
||||||
|
list[AresQueryCAAResult],
|
||||||
|
AresQueryPTRResult,
|
||||||
|
list[ConvertedRecord], # For ANY query type
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_record(record: pycares.DNSRecord) -> ConvertedRecord:
|
||||||
|
"""Convert a single DNS record to pycares 4.x compatible format."""
|
||||||
|
ttl = record.ttl
|
||||||
|
record_type = record.type
|
||||||
|
|
||||||
|
if record_type == pycares.QUERY_TYPE_A:
|
||||||
|
a_data = cast(pycares.ARecordData, record.data)
|
||||||
|
return AresQueryAResult(host=a_data.addr, ttl=ttl)
|
||||||
|
if record_type == pycares.QUERY_TYPE_AAAA:
|
||||||
|
aaaa_data = cast(pycares.AAAARecordData, record.data)
|
||||||
|
return AresQueryAAAAResult(host=aaaa_data.addr, ttl=ttl)
|
||||||
|
if record_type == pycares.QUERY_TYPE_CNAME:
|
||||||
|
cname_data = cast(pycares.CNAMERecordData, record.data)
|
||||||
|
return AresQueryCNAMEResult(cname=cname_data.cname, ttl=ttl)
|
||||||
|
if record_type == pycares.QUERY_TYPE_MX:
|
||||||
|
mx_data = cast(pycares.MXRecordData, record.data)
|
||||||
|
return AresQueryMXResult(
|
||||||
|
host=mx_data.exchange, priority=mx_data.priority, ttl=ttl
|
||||||
|
)
|
||||||
|
if record_type == pycares.QUERY_TYPE_NS:
|
||||||
|
ns_data = cast(pycares.NSRecordData, record.data)
|
||||||
|
return AresQueryNSResult(host=ns_data.nsdname, ttl=ttl)
|
||||||
|
if record_type == pycares.QUERY_TYPE_TXT:
|
||||||
|
txt_data = cast(pycares.TXTRecordData, record.data)
|
||||||
|
return AresQueryTXTResult(text=_maybe_str(txt_data.data), ttl=ttl)
|
||||||
|
if record_type == pycares.QUERY_TYPE_SOA:
|
||||||
|
soa_data = cast(pycares.SOARecordData, record.data)
|
||||||
|
return AresQuerySOAResult(
|
||||||
|
nsname=soa_data.mname,
|
||||||
|
hostmaster=soa_data.rname,
|
||||||
|
serial=soa_data.serial,
|
||||||
|
refresh=soa_data.refresh,
|
||||||
|
retry=soa_data.retry,
|
||||||
|
expires=soa_data.expire,
|
||||||
|
minttl=soa_data.minimum,
|
||||||
|
ttl=ttl,
|
||||||
|
)
|
||||||
|
if record_type == pycares.QUERY_TYPE_SRV:
|
||||||
|
srv_data = cast(pycares.SRVRecordData, record.data)
|
||||||
|
return AresQuerySRVResult(
|
||||||
|
host=srv_data.target,
|
||||||
|
port=srv_data.port,
|
||||||
|
priority=srv_data.priority,
|
||||||
|
weight=srv_data.weight,
|
||||||
|
ttl=ttl,
|
||||||
|
)
|
||||||
|
if record_type == pycares.QUERY_TYPE_NAPTR:
|
||||||
|
naptr_data = cast(pycares.NAPTRRecordData, record.data)
|
||||||
|
return AresQueryNAPTRResult(
|
||||||
|
order=naptr_data.order,
|
||||||
|
preference=naptr_data.preference,
|
||||||
|
flags=naptr_data.flags,
|
||||||
|
service=naptr_data.service,
|
||||||
|
regex=naptr_data.regexp,
|
||||||
|
replacement=naptr_data.replacement,
|
||||||
|
ttl=ttl,
|
||||||
|
)
|
||||||
|
if record_type == pycares.QUERY_TYPE_CAA:
|
||||||
|
caa_data = cast(pycares.CAARecordData, record.data)
|
||||||
|
return AresQueryCAAResult(
|
||||||
|
critical=caa_data.critical,
|
||||||
|
property=caa_data.tag,
|
||||||
|
value=caa_data.value,
|
||||||
|
ttl=ttl,
|
||||||
|
)
|
||||||
|
if record_type == pycares.QUERY_TYPE_PTR:
|
||||||
|
ptr_data = cast(pycares.PTRRecordData, record.data)
|
||||||
|
return AresQueryPTRResult(name=ptr_data.dname, ttl=ttl, aliases=[])
|
||||||
|
# Return raw record for unknown types
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def convert_result(dns_result: pycares.DNSResult, qtype: int) -> QueryResult:
|
||||||
|
"""Convert pycares 5.x DNSResult to pycares 4.x compatible format."""
|
||||||
|
# For ANY - convert all records and return mixed list
|
||||||
|
if qtype == pycares.QUERY_TYPE_ANY:
|
||||||
|
return [_convert_record(record) for record in dns_result.answer]
|
||||||
|
|
||||||
|
results: list[ConvertedRecord] = []
|
||||||
|
|
||||||
|
for record in dns_result.answer:
|
||||||
|
record_type = record.type
|
||||||
|
|
||||||
|
# Filter by query type since answer can contain other types
|
||||||
|
# (e.g., CNAME records when querying for A/AAAA)
|
||||||
|
if record_type != qtype:
|
||||||
|
continue
|
||||||
|
|
||||||
|
converted = _convert_record(record)
|
||||||
|
|
||||||
|
# CNAME, SOA, and PTR return single result, not list
|
||||||
|
if record_type in _SINGLE_RESULT_QTYPES:
|
||||||
|
return cast(QueryResult, converted)
|
||||||
|
|
||||||
|
results.append(converted)
|
||||||
|
|
||||||
|
# NOERROR/NODATA: c-ares delivered ARES_SUCCESS but the answer has no
|
||||||
|
# records of the queried type. pycares 4.x raised ARES_ENODATA here;
|
||||||
|
# without this branch single-result qtypes (CNAME/SOA/PTR) would
|
||||||
|
# resolve to [] and crash callers reading .name/.cname/.nsname.
|
||||||
|
if not results:
|
||||||
|
raise error.DNSError(
|
||||||
|
pycares.errno.ARES_ENODATA,
|
||||||
|
pycares.errno.strerror(pycares.errno.ARES_ENODATA),
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
62
dashboard/venv/lib/python3.12/site-packages/aiodns/error.py
Normal file
62
dashboard/venv/lib/python3.12/site-packages/aiodns/error.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from pycares.errno import (
|
||||||
|
ARES_EADDRGETNETWORKPARAMS,
|
||||||
|
ARES_EBADFAMILY,
|
||||||
|
ARES_EBADFLAGS,
|
||||||
|
ARES_EBADHINTS,
|
||||||
|
ARES_EBADNAME,
|
||||||
|
ARES_EBADQUERY,
|
||||||
|
ARES_EBADRESP,
|
||||||
|
ARES_EBADSTR,
|
||||||
|
ARES_ECANCELLED,
|
||||||
|
ARES_ECONNREFUSED,
|
||||||
|
ARES_EDESTRUCTION,
|
||||||
|
ARES_EFILE,
|
||||||
|
ARES_EFORMERR,
|
||||||
|
ARES_ELOADIPHLPAPI,
|
||||||
|
ARES_ENODATA,
|
||||||
|
ARES_ENOMEM,
|
||||||
|
ARES_ENONAME,
|
||||||
|
ARES_ENOTFOUND,
|
||||||
|
ARES_ENOTIMP,
|
||||||
|
ARES_ENOTINITIALIZED,
|
||||||
|
ARES_EOF,
|
||||||
|
ARES_EREFUSED,
|
||||||
|
ARES_ESERVFAIL,
|
||||||
|
ARES_ESERVICE,
|
||||||
|
ARES_ETIMEOUT,
|
||||||
|
ARES_SUCCESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ARES_EADDRGETNETWORKPARAMS',
|
||||||
|
'ARES_EBADFAMILY',
|
||||||
|
'ARES_EBADFLAGS',
|
||||||
|
'ARES_EBADHINTS',
|
||||||
|
'ARES_EBADNAME',
|
||||||
|
'ARES_EBADQUERY',
|
||||||
|
'ARES_EBADRESP',
|
||||||
|
'ARES_EBADSTR',
|
||||||
|
'ARES_ECANCELLED',
|
||||||
|
'ARES_ECONNREFUSED',
|
||||||
|
'ARES_EDESTRUCTION',
|
||||||
|
'ARES_EFILE',
|
||||||
|
'ARES_EFORMERR',
|
||||||
|
'ARES_ELOADIPHLPAPI',
|
||||||
|
'ARES_ENODATA',
|
||||||
|
'ARES_ENOMEM',
|
||||||
|
'ARES_ENONAME',
|
||||||
|
'ARES_ENOTFOUND',
|
||||||
|
'ARES_ENOTIMP',
|
||||||
|
'ARES_ENOTINITIALIZED',
|
||||||
|
'ARES_EOF',
|
||||||
|
'ARES_EREFUSED',
|
||||||
|
'ARES_ESERVFAIL',
|
||||||
|
'ARES_ESERVICE',
|
||||||
|
'ARES_ETIMEOUT',
|
||||||
|
'ARES_SUCCESS',
|
||||||
|
'DNSError',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class DNSError(Exception):
|
||||||
|
"""Base class for all DNS errors."""
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: aiohappyeyeballs
|
||||||
|
Version: 2.6.2
|
||||||
|
Summary: Happy Eyeballs for asyncio
|
||||||
|
License: PSF-2.0
|
||||||
|
License-File: LICENSE
|
||||||
|
Author: J. Nick Koston
|
||||||
|
Author-email: nick@koston.org
|
||||||
|
Requires-Python: >=3.10
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Natural Language :: English
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Topic :: Software Development :: Libraries
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||||
|
Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues
|
||||||
|
Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md
|
||||||
|
Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io
|
||||||
|
Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
|
||||||
|
# aiohappyeyeballs
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/aio-libs/aiohappyeyeballs/actions/workflows/ci.yml?query=branch%3Amain">
|
||||||
|
<img src="https://img.shields.io/github/actions/workflow/status/aio-libs/aiohappyeyeballs/ci-cd.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
||||||
|
</a>
|
||||||
|
<a href="https://aiohappyeyeballs.readthedocs.io">
|
||||||
|
<img src="https://img.shields.io/readthedocs/aiohappyeyeballs.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
||||||
|
</a>
|
||||||
|
<a href="https://codecov.io/gh/aio-libs/aiohappyeyeballs">
|
||||||
|
<img src="https://img.shields.io/codecov/c/github/aio-libs/aiohappyeyeballs.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://python-poetry.org/">
|
||||||
|
<img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/astral-sh/ruff">
|
||||||
|
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/pre-commit/pre-commit">
|
||||||
|
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://pypi.org/project/aiohappyeyeballs/">
|
||||||
|
<img src="https://img.shields.io/pypi/v/aiohappyeyeballs.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
||||||
|
</a>
|
||||||
|
<img src="https://img.shields.io/pypi/pyversions/aiohappyeyeballs.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
||||||
|
<img src="https://img.shields.io/pypi/l/aiohappyeyeballs.svg?style=flat-square" alt="License">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Documentation**: <a href="https://aiohappyeyeballs.readthedocs.io" target="_blank">https://aiohappyeyeballs.readthedocs.io </a>
|
||||||
|
|
||||||
|
**Source Code**: <a href="https://github.com/aio-libs/aiohappyeyeballs" target="_blank">https://github.com/aio-libs/aiohappyeyeballs </a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
|
||||||
|
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
|
||||||
|
|
||||||
|
## Use case
|
||||||
|
|
||||||
|
This library exists to allow connecting with
|
||||||
|
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
|
||||||
|
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
|
||||||
|
when you
|
||||||
|
already have a list of addrinfo and not a DNS name.
|
||||||
|
|
||||||
|
The stdlib version of `loop.create_connection()`
|
||||||
|
will only work when you pass in an unresolved name which
|
||||||
|
is not a good fit when using DNS caching or resolving
|
||||||
|
names via another method such as `zeroconf`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install this via pip (or your favourite package manager):
|
||||||
|
|
||||||
|
`pip install aiohappyeyeballs`
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[aiohappyeyeballs is licensed under the same terms as cpython itself.](https://github.com/python/cpython/blob/main/LICENSE)
|
||||||
|
|
||||||
|
## Example usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
addr_infos = await loop.getaddrinfo("example.org", 80)
|
||||||
|
|
||||||
|
socket = await start_connection(addr_infos)
|
||||||
|
socket = await start_connection(addr_infos, local_addr_infos=local_addr_infos, happy_eyeballs_delay=0.2)
|
||||||
|
|
||||||
|
transport, protocol = await loop.create_connection(
|
||||||
|
MyProtocol, sock=socket, ...)
|
||||||
|
|
||||||
|
# Remove the first address for each family from addr_info
|
||||||
|
pop_addr_infos_interleave(addr_info, 1)
|
||||||
|
|
||||||
|
# Remove all matching address from addr_info
|
||||||
|
remove_addr_infos(addr_info, "dead::beef::")
|
||||||
|
|
||||||
|
# Convert a local_addr to local_addr_infos
|
||||||
|
local_addr_infos = addr_to_addr_infos(("127.0.0.1",0))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This package contains code from cpython and is licensed under the same terms as cpython itself.
|
||||||
|
|
||||||
|
This package was created with
|
||||||
|
[Copier](https://copier.readthedocs.io/) and the
|
||||||
|
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
||||||
|
project template.
|
||||||
|
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
aiohappyeyeballs-2.6.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
aiohappyeyeballs-2.6.2.dist-info/METADATA,sha256=cqs2VY8TwE2e_4qqnW409Si3suvj-aM-8dCiWG2Angk,5888
|
||||||
|
aiohappyeyeballs-2.6.2.dist-info/RECORD,,
|
||||||
|
aiohappyeyeballs-2.6.2.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
||||||
|
aiohappyeyeballs-2.6.2.dist-info/licenses/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
|
||||||
|
aiohappyeyeballs/__init__.py,sha256=Af9ADZj3BWLfGaA7ITOFPlKIenh3ozSNWL3yezSZ2Jw,361
|
||||||
|
aiohappyeyeballs/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/_staggered.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/impl.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/types.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/__pycache__/utils.cpython-312.pyc,,
|
||||||
|
aiohappyeyeballs/_staggered.py,sha256=aj3cSwHEDX88UMfO9bUau9tfrRAszhjg99dpEMiAOGM,6698
|
||||||
|
aiohappyeyeballs/impl.py,sha256=TIkAK4xfACvKBp1s7DAKSobHefUTOC2HGE-n0tOthRk,9667
|
||||||
|
aiohappyeyeballs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
aiohappyeyeballs/types.py,sha256=_8JmHFix6MeM1e7hRP7BleEaGy93GswGtzQv068zKY8,288
|
||||||
|
aiohappyeyeballs/utils.py,sha256=dPAcNcrU_VhaTolTEEA94hgh9ONDN9_dYT4Xo9ORqQw,2922
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: poetry-core 2.4.0
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
@ -0,0 +1,279 @@
|
|||||||
|
A. HISTORY OF THE SOFTWARE
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||||
|
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||||
|
as a successor of a language called ABC. Guido remains Python's
|
||||||
|
principal author, although it includes many contributions from others.
|
||||||
|
|
||||||
|
In 1995, Guido continued his work on Python at the Corporation for
|
||||||
|
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||||
|
in Reston, Virginia where he released several versions of the
|
||||||
|
software.
|
||||||
|
|
||||||
|
In May 2000, Guido and the Python core development team moved to
|
||||||
|
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||||
|
year, the PythonLabs team moved to Digital Creations, which became
|
||||||
|
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||||
|
https://www.python.org/psf/) was formed, a non-profit organization
|
||||||
|
created specifically to own Python-related Intellectual Property.
|
||||||
|
Zope Corporation was a sponsoring member of the PSF.
|
||||||
|
|
||||||
|
All Python releases are Open Source (see https://opensource.org for
|
||||||
|
the Open Source Definition). Historically, most, but not all, Python
|
||||||
|
releases have also been GPL-compatible; the table below summarizes
|
||||||
|
the various releases.
|
||||||
|
|
||||||
|
Release Derived Year Owner GPL-
|
||||||
|
from compatible? (1)
|
||||||
|
|
||||||
|
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||||
|
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||||
|
1.6 1.5.2 2000 CNRI no
|
||||||
|
2.0 1.6 2000 BeOpen.com no
|
||||||
|
1.6.1 1.6 2001 CNRI yes (2)
|
||||||
|
2.1 2.0+1.6.1 2001 PSF no
|
||||||
|
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||||
|
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||||
|
2.1.2 2.1.1 2002 PSF yes
|
||||||
|
2.1.3 2.1.2 2002 PSF yes
|
||||||
|
2.2 and above 2.1.1 2001-now PSF yes
|
||||||
|
|
||||||
|
Footnotes:
|
||||||
|
|
||||||
|
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||||
|
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||||
|
a modified version without making your changes open source. The
|
||||||
|
GPL-compatible licenses make it possible to combine Python with
|
||||||
|
other software that is released under the GPL; the others don't.
|
||||||
|
|
||||||
|
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||||
|
because its license has a choice of law clause. According to
|
||||||
|
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||||
|
is "not incompatible" with the GPL.
|
||||||
|
|
||||||
|
Thanks to the many outside volunteers who have worked under Guido's
|
||||||
|
direction to make these releases possible.
|
||||||
|
|
||||||
|
|
||||||
|
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
Python software and documentation are licensed under the
|
||||||
|
Python Software Foundation License Version 2.
|
||||||
|
|
||||||
|
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||||
|
the documentation are dual licensed under the PSF License Version 2
|
||||||
|
and the Zero-Clause BSD license.
|
||||||
|
|
||||||
|
Some software incorporated into Python is under different licenses.
|
||||||
|
The licenses are listed with code falling under that license.
|
||||||
|
|
||||||
|
|
||||||
|
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using this software ("Python") in source or binary form and
|
||||||
|
its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||||
|
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||||
|
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||||
|
distribute, and otherwise use Python alone or in any derivative version,
|
||||||
|
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||||
|
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||||
|
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
|
||||||
|
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||||
|
prepared by Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python.
|
||||||
|
|
||||||
|
4. PSF is making Python available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||||
|
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||||
|
Individual or Organization ("Licensee") accessing and otherwise using
|
||||||
|
this software in source or binary form and its associated
|
||||||
|
documentation ("the Software").
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this BeOpen Python License
|
||||||
|
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||||
|
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||||
|
and/or display publicly, prepare derivative works, distribute, and
|
||||||
|
otherwise use the Software alone or in any derivative version,
|
||||||
|
provided, however, that the BeOpen Python License is retained in the
|
||||||
|
Software, alone or in any derivative version prepared by Licensee.
|
||||||
|
|
||||||
|
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||||
|
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||||
|
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||||
|
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||||
|
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
5. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
6. This License Agreement shall be governed by and interpreted in all
|
||||||
|
respects by the law of the State of California, excluding conflict of
|
||||||
|
law provisions. Nothing in this License Agreement shall be deemed to
|
||||||
|
create any relationship of agency, partnership, or joint venture
|
||||||
|
between BeOpen and Licensee. This License Agreement does not grant
|
||||||
|
permission to use BeOpen trademarks or trade names in a trademark
|
||||||
|
sense to endorse or promote products or services of Licensee, or any
|
||||||
|
third party. As an exception, the "BeOpen Python" logos available at
|
||||||
|
http://www.pythonlabs.com/logos.html may be used according to the
|
||||||
|
permissions granted on that web page.
|
||||||
|
|
||||||
|
7. By copying, installing or otherwise using the software, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||||
|
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||||
|
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||||
|
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||||
|
source or binary form and its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||||
|
alone or in any derivative version, provided, however, that CNRI's
|
||||||
|
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||||
|
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||||
|
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||||
|
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||||
|
Agreement, Licensee may substitute the following text (omitting the
|
||||||
|
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||||
|
conditions in CNRI's License Agreement. This Agreement together with
|
||||||
|
Python 1.6.1 may be located on the internet using the following
|
||||||
|
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||||
|
Agreement may also be obtained from a proxy server on the internet
|
||||||
|
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python 1.6.1.
|
||||||
|
|
||||||
|
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||||
|
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. This License Agreement shall be governed by the federal
|
||||||
|
intellectual property law of the United States, including without
|
||||||
|
limitation the federal copyright law, and, to the extent such
|
||||||
|
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||||
|
Virginia, excluding Virginia's conflict of law provisions.
|
||||||
|
Notwithstanding the foregoing, with regard to derivative works based
|
||||||
|
on Python 1.6.1 that incorporate non-separable material that was
|
||||||
|
previously distributed under the GNU General Public License (GPL), the
|
||||||
|
law of the Commonwealth of Virginia shall govern this License
|
||||||
|
Agreement only as to issues arising under or with respect to
|
||||||
|
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||||
|
License Agreement shall be deemed to create any relationship of
|
||||||
|
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||||
|
License Agreement does not grant permission to use CNRI trademarks or
|
||||||
|
trade name in a trademark sense to endorse or promote products or
|
||||||
|
services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||||
|
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||||
|
bound by the terms and conditions of this License Agreement.
|
||||||
|
|
||||||
|
ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||||
|
The Netherlands. All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software and its
|
||||||
|
documentation for any purpose and without fee is hereby granted,
|
||||||
|
provided that the above copyright notice appear in all copies and that
|
||||||
|
both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation, and that the name of Stichting Mathematisch
|
||||||
|
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
|
||||||
|
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||||
|
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||||
|
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||||
|
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
PERFORMANCE OF THIS SOFTWARE.
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
__version__ = "2.6.2"
|
||||||
|
|
||||||
|
from .impl import start_connection
|
||||||
|
from .types import AddrInfoType, SocketFactoryType
|
||||||
|
from .utils import addr_to_addr_infos, pop_addr_infos_interleave, remove_addr_infos
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"AddrInfoType",
|
||||||
|
"SocketFactoryType",
|
||||||
|
"addr_to_addr_infos",
|
||||||
|
"pop_addr_infos_interleave",
|
||||||
|
"remove_addr_infos",
|
||||||
|
"start_connection",
|
||||||
|
)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,197 @@
|
|||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
TypeVar,
|
||||||
|
)
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
RE_RAISE_EXCEPTIONS = (SystemExit, KeyboardInterrupt)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_result(wait_next: "asyncio.Future[None]") -> None:
|
||||||
|
"""Set the result of a future if it is not already done."""
|
||||||
|
if not wait_next.done():
|
||||||
|
wait_next.set_result(None)
|
||||||
|
|
||||||
|
|
||||||
|
async def _wait_one(
|
||||||
|
futures: "Iterable[asyncio.Future[Any]]",
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
) -> _T:
|
||||||
|
"""Wait for the first future to complete."""
|
||||||
|
wait_next = loop.create_future()
|
||||||
|
|
||||||
|
def _on_completion(fut: "asyncio.Future[Any]") -> None:
|
||||||
|
if not wait_next.done():
|
||||||
|
wait_next.set_result(fut)
|
||||||
|
|
||||||
|
for f in futures:
|
||||||
|
f.add_done_callback(_on_completion)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await wait_next
|
||||||
|
finally:
|
||||||
|
for f in futures:
|
||||||
|
f.remove_done_callback(_on_completion)
|
||||||
|
|
||||||
|
|
||||||
|
async def staggered_race(
|
||||||
|
coro_fns: Iterable[Callable[[], Awaitable[_T]]],
|
||||||
|
delay: float | None,
|
||||||
|
*,
|
||||||
|
loop: asyncio.AbstractEventLoop | None = None,
|
||||||
|
) -> tuple[_T | None, int | None, list[BaseException | None]]:
|
||||||
|
"""
|
||||||
|
Run coroutines with staggered start times and take the first to finish.
|
||||||
|
|
||||||
|
This method takes an iterable of coroutine functions. The first one is
|
||||||
|
started immediately. From then on, whenever the immediately preceding one
|
||||||
|
fails (raises an exception), or when *delay* seconds has passed, the next
|
||||||
|
coroutine is started. This continues until one of the coroutines complete
|
||||||
|
successfully, in which case all others are cancelled, or until all
|
||||||
|
coroutines fail.
|
||||||
|
|
||||||
|
The coroutines provided should be well-behaved in the following way:
|
||||||
|
|
||||||
|
* They should only ``return`` if completed successfully.
|
||||||
|
|
||||||
|
* They should always raise an exception if they did not complete
|
||||||
|
successfully. In particular, if they handle cancellation, they should
|
||||||
|
probably reraise, like this::
|
||||||
|
|
||||||
|
try:
|
||||||
|
# do work
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# undo partially completed work
|
||||||
|
raise
|
||||||
|
|
||||||
|
Args:
|
||||||
|
----
|
||||||
|
coro_fns: an iterable of coroutine functions, i.e. callables that
|
||||||
|
return a coroutine object when called. Use ``functools.partial`` or
|
||||||
|
lambdas to pass arguments.
|
||||||
|
|
||||||
|
delay: amount of time, in seconds, between starting coroutines. If
|
||||||
|
``None``, the coroutines will run sequentially.
|
||||||
|
|
||||||
|
loop: the event loop to use. If ``None``, the running loop is used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-------
|
||||||
|
tuple *(winner_result, winner_index, exceptions)* where
|
||||||
|
|
||||||
|
- *winner_result*: the result of the winning coroutine, or ``None``
|
||||||
|
if no coroutines won.
|
||||||
|
|
||||||
|
- *winner_index*: the index of the winning coroutine in
|
||||||
|
``coro_fns``, or ``None`` if no coroutines won. If the winning
|
||||||
|
coroutine may return None on success, *winner_index* can be used
|
||||||
|
to definitively determine whether any coroutine won.
|
||||||
|
|
||||||
|
- *exceptions*: list of exceptions returned by the coroutines.
|
||||||
|
``len(exceptions)`` is equal to the number of coroutines actually
|
||||||
|
started, and the order is the same as in ``coro_fns``. The winning
|
||||||
|
coroutine's entry is ``None``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
loop = loop or asyncio.get_running_loop()
|
||||||
|
exceptions: list[BaseException | None] = []
|
||||||
|
tasks: set[asyncio.Task[tuple[_T, int] | None]] = set()
|
||||||
|
|
||||||
|
async def run_one_coro(
|
||||||
|
coro_fn: Callable[[], Awaitable[_T]],
|
||||||
|
this_index: int,
|
||||||
|
start_next: "asyncio.Future[None]",
|
||||||
|
) -> tuple[_T, int] | None:
|
||||||
|
"""
|
||||||
|
Run a single coroutine.
|
||||||
|
|
||||||
|
If the coroutine fails, set the exception in the exceptions list and
|
||||||
|
start the next coroutine by setting the result of the start_next.
|
||||||
|
|
||||||
|
If the coroutine succeeds, return the result and the index of the
|
||||||
|
coroutine in the coro_fns list.
|
||||||
|
|
||||||
|
If SystemExit or KeyboardInterrupt is raised, re-raise it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = await coro_fn()
|
||||||
|
except RE_RAISE_EXCEPTIONS:
|
||||||
|
raise
|
||||||
|
except BaseException as e:
|
||||||
|
exceptions[this_index] = e
|
||||||
|
_set_result(start_next) # Kickstart the next coroutine
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result, this_index
|
||||||
|
|
||||||
|
start_next_timer: asyncio.TimerHandle | None = None
|
||||||
|
start_next: asyncio.Future[None] | None
|
||||||
|
task: asyncio.Task[tuple[_T, int] | None]
|
||||||
|
done: asyncio.Future[None] | asyncio.Task[tuple[_T, int] | None]
|
||||||
|
coro_iter = iter(coro_fns)
|
||||||
|
this_index = -1
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if coro_fn := next(coro_iter, None):
|
||||||
|
this_index += 1
|
||||||
|
exceptions.append(None)
|
||||||
|
start_next = loop.create_future()
|
||||||
|
task = loop.create_task(run_one_coro(coro_fn, this_index, start_next))
|
||||||
|
tasks.add(task)
|
||||||
|
start_next_timer = (
|
||||||
|
loop.call_later(delay, _set_result, start_next) if delay else None
|
||||||
|
)
|
||||||
|
elif not tasks:
|
||||||
|
# We exhausted the coro_fns list and no tasks are running
|
||||||
|
# so we have no winner and all coroutines failed.
|
||||||
|
break
|
||||||
|
|
||||||
|
while tasks or start_next:
|
||||||
|
done = await _wait_one(
|
||||||
|
(*tasks, start_next) if start_next else tasks, loop
|
||||||
|
)
|
||||||
|
if done is start_next:
|
||||||
|
# The current task has failed or the timer has expired
|
||||||
|
# so we need to start the next task.
|
||||||
|
start_next = None
|
||||||
|
if start_next_timer:
|
||||||
|
start_next_timer.cancel()
|
||||||
|
start_next_timer = None
|
||||||
|
|
||||||
|
# Break out of the task waiting loop to start the next
|
||||||
|
# task.
|
||||||
|
break
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(done, asyncio.Task)
|
||||||
|
|
||||||
|
tasks.remove(done)
|
||||||
|
if winner := done.result():
|
||||||
|
return *winner, exceptions
|
||||||
|
finally:
|
||||||
|
# We either have:
|
||||||
|
# - a winner
|
||||||
|
# - all tasks failed
|
||||||
|
# - a KeyboardInterrupt or SystemExit.
|
||||||
|
|
||||||
|
#
|
||||||
|
# If the timer is still running, cancel it.
|
||||||
|
#
|
||||||
|
if start_next_timer:
|
||||||
|
start_next_timer.cancel()
|
||||||
|
|
||||||
|
#
|
||||||
|
# If there are any tasks left, cancel them and than
|
||||||
|
# wait them so they fill the exceptions list.
|
||||||
|
#
|
||||||
|
for task in tasks:
|
||||||
|
task.cancel()
|
||||||
|
with contextlib.suppress(asyncio.CancelledError):
|
||||||
|
await task
|
||||||
|
|
||||||
|
return None, None, exceptions
|
||||||
@ -0,0 +1,261 @@
|
|||||||
|
"""Base implementation."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import collections
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import itertools
|
||||||
|
import socket
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from . import _staggered
|
||||||
|
from .types import AddrInfoType, SocketFactoryType
|
||||||
|
|
||||||
|
|
||||||
|
async def start_connection(
|
||||||
|
addr_infos: Sequence[AddrInfoType],
|
||||||
|
*,
|
||||||
|
local_addr_infos: Sequence[AddrInfoType] | None = None,
|
||||||
|
happy_eyeballs_delay: float | None = None,
|
||||||
|
interleave: int | None = None,
|
||||||
|
loop: asyncio.AbstractEventLoop | None = None,
|
||||||
|
socket_factory: SocketFactoryType | None = None,
|
||||||
|
) -> socket.socket:
|
||||||
|
"""
|
||||||
|
Connect to a TCP server.
|
||||||
|
|
||||||
|
Create a socket connection to a specified destination. The
|
||||||
|
destination is specified as a list of AddrInfoType tuples as
|
||||||
|
returned from getaddrinfo().
|
||||||
|
|
||||||
|
The arguments are, in order:
|
||||||
|
|
||||||
|
* ``family``: the address family, e.g. ``socket.AF_INET`` or
|
||||||
|
``socket.AF_INET6``.
|
||||||
|
* ``type``: the socket type, e.g. ``socket.SOCK_STREAM`` or
|
||||||
|
``socket.SOCK_DGRAM``.
|
||||||
|
* ``proto``: the protocol, e.g. ``socket.IPPROTO_TCP`` or
|
||||||
|
``socket.IPPROTO_UDP``.
|
||||||
|
* ``canonname``: the canonical name of the address, e.g.
|
||||||
|
``"www.python.org"``.
|
||||||
|
* ``sockaddr``: the socket address
|
||||||
|
|
||||||
|
This method is a coroutine which will try to establish the connection
|
||||||
|
in the background. When successful, the coroutine returns a
|
||||||
|
socket.
|
||||||
|
|
||||||
|
The expected use case is to use this method in conjunction with
|
||||||
|
loop.create_connection() to establish a connection to a server::
|
||||||
|
|
||||||
|
socket = await start_connection(addr_infos)
|
||||||
|
transport, protocol = await loop.create_connection(
|
||||||
|
MyProtocol, sock=socket, ...)
|
||||||
|
"""
|
||||||
|
if not addr_infos:
|
||||||
|
raise ValueError("addr_infos must not be empty")
|
||||||
|
|
||||||
|
current_loop = loop or asyncio.get_running_loop()
|
||||||
|
|
||||||
|
single_addr_info = len(addr_infos) == 1
|
||||||
|
|
||||||
|
if happy_eyeballs_delay is not None and interleave is None:
|
||||||
|
# If using happy eyeballs, default to interleave addresses by family
|
||||||
|
interleave = 1
|
||||||
|
|
||||||
|
if interleave and not single_addr_info:
|
||||||
|
addr_infos = _interleave_addrinfos(addr_infos, interleave)
|
||||||
|
|
||||||
|
sock: socket.socket | None = None
|
||||||
|
# uvloop can raise RuntimeError instead of OSError
|
||||||
|
exceptions: list[list[OSError | RuntimeError]] = []
|
||||||
|
if happy_eyeballs_delay is None or single_addr_info:
|
||||||
|
# not using happy eyeballs
|
||||||
|
for addrinfo in addr_infos:
|
||||||
|
try:
|
||||||
|
sock = await _connect_sock(
|
||||||
|
current_loop,
|
||||||
|
exceptions,
|
||||||
|
addrinfo,
|
||||||
|
local_addr_infos,
|
||||||
|
None,
|
||||||
|
socket_factory,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except (RuntimeError, OSError):
|
||||||
|
continue
|
||||||
|
else: # using happy eyeballs
|
||||||
|
open_sockets: set[socket.socket] = set()
|
||||||
|
try:
|
||||||
|
sock, _, _ = await _staggered.staggered_race(
|
||||||
|
(
|
||||||
|
functools.partial(
|
||||||
|
_connect_sock,
|
||||||
|
current_loop,
|
||||||
|
exceptions,
|
||||||
|
addrinfo,
|
||||||
|
local_addr_infos,
|
||||||
|
open_sockets,
|
||||||
|
socket_factory,
|
||||||
|
)
|
||||||
|
for addrinfo in addr_infos
|
||||||
|
),
|
||||||
|
happy_eyeballs_delay,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# If we have a winner, staggered_race will
|
||||||
|
# cancel the other tasks, however there is a
|
||||||
|
# small race window where any of the other tasks
|
||||||
|
# can be done before they are cancelled which
|
||||||
|
# will leave the socket open. To avoid this problem
|
||||||
|
# we pass a set to _connect_sock to keep track of
|
||||||
|
# the open sockets and close them here if there
|
||||||
|
# are any "runner up" sockets.
|
||||||
|
for s in open_sockets:
|
||||||
|
if s is not sock:
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
s.close()
|
||||||
|
open_sockets = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
if sock is None:
|
||||||
|
all_exceptions = [exc for sub in exceptions for exc in sub]
|
||||||
|
try:
|
||||||
|
first_exception = all_exceptions[0]
|
||||||
|
if len(all_exceptions) == 1:
|
||||||
|
raise first_exception
|
||||||
|
else:
|
||||||
|
# If they all have the same str(), raise one.
|
||||||
|
model = str(first_exception)
|
||||||
|
if all(str(exc) == model for exc in all_exceptions):
|
||||||
|
raise first_exception
|
||||||
|
# Raise a combined exception so the user can see all
|
||||||
|
# the various error messages.
|
||||||
|
msg = "Multiple exceptions: {}".format(
|
||||||
|
", ".join(str(exc) for exc in all_exceptions)
|
||||||
|
)
|
||||||
|
# If the errno is the same for all exceptions, raise
|
||||||
|
# an OSError with that errno.
|
||||||
|
if isinstance(first_exception, OSError):
|
||||||
|
first_errno = first_exception.errno
|
||||||
|
if all(
|
||||||
|
isinstance(exc, OSError) and exc.errno == first_errno
|
||||||
|
for exc in all_exceptions
|
||||||
|
):
|
||||||
|
raise OSError(first_errno, msg)
|
||||||
|
elif isinstance(first_exception, RuntimeError) and all(
|
||||||
|
isinstance(exc, RuntimeError) for exc in all_exceptions
|
||||||
|
):
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
# We have a mix of OSError and RuntimeError
|
||||||
|
# so we have to pick which one to raise.
|
||||||
|
# and we raise OSError for compatibility
|
||||||
|
raise OSError(msg)
|
||||||
|
finally:
|
||||||
|
all_exceptions = None # type: ignore[assignment]
|
||||||
|
exceptions = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
async def _connect_sock(
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
exceptions: list[list[OSError | RuntimeError]],
|
||||||
|
addr_info: AddrInfoType,
|
||||||
|
local_addr_infos: Sequence[AddrInfoType] | None = None,
|
||||||
|
open_sockets: set[socket.socket] | None = None,
|
||||||
|
socket_factory: SocketFactoryType | None = None,
|
||||||
|
) -> socket.socket:
|
||||||
|
"""
|
||||||
|
Create, bind and connect one socket.
|
||||||
|
|
||||||
|
If open_sockets is passed, add the socket to the set of open sockets.
|
||||||
|
Any failure caught here will remove the socket from the set and close it.
|
||||||
|
|
||||||
|
Callers can use this set to close any sockets that are not the winner
|
||||||
|
of all staggered tasks in the result there are runner up sockets aka
|
||||||
|
multiple winners.
|
||||||
|
"""
|
||||||
|
my_exceptions: list[OSError | RuntimeError] = []
|
||||||
|
exceptions.append(my_exceptions)
|
||||||
|
family, type_, proto, _, address = addr_info
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
if socket_factory is not None:
|
||||||
|
sock = socket_factory(addr_info)
|
||||||
|
else:
|
||||||
|
sock = socket.socket(family=family, type=type_, proto=proto)
|
||||||
|
if open_sockets is not None:
|
||||||
|
open_sockets.add(sock)
|
||||||
|
sock.setblocking(False)
|
||||||
|
if local_addr_infos is not None:
|
||||||
|
for lfamily, _, _, _, laddr in local_addr_infos:
|
||||||
|
# skip local addresses of different family
|
||||||
|
if lfamily != family:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sock.bind(laddr)
|
||||||
|
break
|
||||||
|
except OSError as exc:
|
||||||
|
msg = (
|
||||||
|
f"error while attempting to bind on "
|
||||||
|
f"address {laddr!r}: "
|
||||||
|
f"{(exc.strerror or '').lower()}"
|
||||||
|
)
|
||||||
|
exc = OSError(exc.errno, msg)
|
||||||
|
my_exceptions.append(exc)
|
||||||
|
else: # all bind attempts failed
|
||||||
|
if my_exceptions:
|
||||||
|
raise my_exceptions.pop()
|
||||||
|
else:
|
||||||
|
raise OSError(f"no matching local address with {family=} found")
|
||||||
|
await loop.sock_connect(sock, address)
|
||||||
|
return sock
|
||||||
|
except (RuntimeError, OSError) as exc:
|
||||||
|
my_exceptions.append(exc)
|
||||||
|
if sock is not None:
|
||||||
|
if open_sockets is not None:
|
||||||
|
open_sockets.remove(sock)
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError as e:
|
||||||
|
my_exceptions.append(e)
|
||||||
|
raise
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
if sock is not None:
|
||||||
|
if open_sockets is not None:
|
||||||
|
open_sockets.remove(sock)
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError as e:
|
||||||
|
my_exceptions.append(e)
|
||||||
|
raise
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
exceptions = my_exceptions = None # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
|
def _interleave_addrinfos(
|
||||||
|
addrinfos: Sequence[AddrInfoType], first_address_family_count: int = 1
|
||||||
|
) -> list[AddrInfoType]:
|
||||||
|
"""Interleave list of addrinfo tuples by family."""
|
||||||
|
# Group addresses by family
|
||||||
|
addrinfos_by_family: collections.OrderedDict[int, list[AddrInfoType]] = (
|
||||||
|
collections.OrderedDict()
|
||||||
|
)
|
||||||
|
for addr in addrinfos:
|
||||||
|
family = addr[0]
|
||||||
|
if family not in addrinfos_by_family:
|
||||||
|
addrinfos_by_family[family] = []
|
||||||
|
addrinfos_by_family[family].append(addr)
|
||||||
|
addrinfos_lists = list(addrinfos_by_family.values())
|
||||||
|
|
||||||
|
reordered: list[AddrInfoType] = []
|
||||||
|
if first_address_family_count > 1:
|
||||||
|
reordered.extend(addrinfos_lists[0][: first_address_family_count - 1])
|
||||||
|
del addrinfos_lists[0][: first_address_family_count - 1]
|
||||||
|
reordered.extend(
|
||||||
|
a
|
||||||
|
for a in itertools.chain.from_iterable(itertools.zip_longest(*addrinfos_lists))
|
||||||
|
if a is not None
|
||||||
|
)
|
||||||
|
return reordered
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
"""Types for aiohappyeyeballs."""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
AddrInfoType = tuple[
|
||||||
|
int | socket.AddressFamily,
|
||||||
|
int | socket.SocketKind,
|
||||||
|
int,
|
||||||
|
str,
|
||||||
|
tuple, # type: ignore[type-arg]
|
||||||
|
]
|
||||||
|
|
||||||
|
SocketFactoryType = Callable[[AddrInfoType], socket.socket]
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
"""Utility functions for aiohappyeyeballs."""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from .types import AddrInfoType
|
||||||
|
|
||||||
|
|
||||||
|
def addr_to_addr_infos(
|
||||||
|
addr: tuple[str, int, int, int] | tuple[str, int, int] | tuple[str, int] | None,
|
||||||
|
) -> list[AddrInfoType] | None:
|
||||||
|
"""Convert an address tuple to a list of addr_info tuples."""
|
||||||
|
if addr is None:
|
||||||
|
return None
|
||||||
|
host = addr[0]
|
||||||
|
port = addr[1]
|
||||||
|
is_ipv6 = ":" in host
|
||||||
|
if is_ipv6:
|
||||||
|
flowinfo = 0
|
||||||
|
scopeid = 0
|
||||||
|
addr_len = len(addr)
|
||||||
|
if addr_len >= 4:
|
||||||
|
scopeid = addr[3] # type: ignore[misc]
|
||||||
|
if addr_len >= 3:
|
||||||
|
flowinfo = addr[2] # type: ignore[misc]
|
||||||
|
addr = (host, port, flowinfo, scopeid)
|
||||||
|
family = socket.AF_INET6
|
||||||
|
else:
|
||||||
|
addr = (host, port)
|
||||||
|
family = socket.AF_INET
|
||||||
|
return [(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)]
|
||||||
|
|
||||||
|
|
||||||
|
def pop_addr_infos_interleave(
|
||||||
|
addr_infos: list[AddrInfoType], interleave: int | None = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Pop addr_info from the list of addr_infos by family up to interleave times.
|
||||||
|
|
||||||
|
The interleave parameter is used to know how many addr_infos for
|
||||||
|
each family should be popped of the top of the list.
|
||||||
|
"""
|
||||||
|
seen: dict[int, int] = {}
|
||||||
|
if interleave is None:
|
||||||
|
interleave = 1
|
||||||
|
to_remove: list[AddrInfoType] = []
|
||||||
|
for addr_info in addr_infos:
|
||||||
|
family = addr_info[0]
|
||||||
|
if family not in seen:
|
||||||
|
seen[family] = 0
|
||||||
|
if seen[family] < interleave:
|
||||||
|
to_remove.append(addr_info)
|
||||||
|
seen[family] += 1
|
||||||
|
for addr_info in to_remove:
|
||||||
|
addr_infos.remove(addr_info)
|
||||||
|
|
||||||
|
|
||||||
|
def _addr_tuple_to_ip_address(
|
||||||
|
addr: tuple[str, int] | tuple[str, int, int, int],
|
||||||
|
) -> tuple[ipaddress.IPv4Address, int] | tuple[ipaddress.IPv6Address, int, int, int]:
|
||||||
|
"""Convert an address tuple to an IPv4Address."""
|
||||||
|
return (ipaddress.ip_address(addr[0]), *addr[1:])
|
||||||
|
|
||||||
|
|
||||||
|
def remove_addr_infos(
|
||||||
|
addr_infos: list[AddrInfoType],
|
||||||
|
addr: tuple[str, int] | tuple[str, int, int, int],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Remove an address from the list of addr_infos.
|
||||||
|
|
||||||
|
The addr value is typically the return value of
|
||||||
|
sock.getpeername().
|
||||||
|
"""
|
||||||
|
bad_addrs_infos: list[AddrInfoType] = []
|
||||||
|
for addr_info in addr_infos:
|
||||||
|
if addr_info[-1] == addr:
|
||||||
|
bad_addrs_infos.append(addr_info)
|
||||||
|
if bad_addrs_infos:
|
||||||
|
for bad_addr_info in bad_addrs_infos:
|
||||||
|
addr_infos.remove(bad_addr_info)
|
||||||
|
return
|
||||||
|
# Slow path in case addr is formatted differently
|
||||||
|
match_addr = _addr_tuple_to_ip_address(addr)
|
||||||
|
for addr_info in addr_infos:
|
||||||
|
if match_addr == _addr_tuple_to_ip_address(addr_info[-1]):
|
||||||
|
bad_addrs_infos.append(addr_info)
|
||||||
|
if bad_addrs_infos:
|
||||||
|
for bad_addr_info in bad_addrs_infos:
|
||||||
|
addr_infos.remove(bad_addr_info)
|
||||||
|
return
|
||||||
|
raise ValueError(f"Address {addr} not found in addr_infos")
|
||||||
@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@ -0,0 +1,257 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: aiohttp
|
||||||
|
Version: 3.14.1
|
||||||
|
Summary: Async http client/server framework (asyncio)
|
||||||
|
Maintainer-email: aiohttp team <team@aiohttp.org>
|
||||||
|
License: Apache-2.0 AND MIT
|
||||||
|
Project-URL: Homepage, https://github.com/aio-libs/aiohttp
|
||||||
|
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
|
||||||
|
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
|
||||||
|
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
||||||
|
Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
|
||||||
|
Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
|
||||||
|
Project-URL: Docs: RTD, https://docs.aiohttp.org
|
||||||
|
Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
|
||||||
|
Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Framework :: AsyncIO
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: Operating System :: MacOS :: MacOS X
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP
|
||||||
|
Requires-Python: >=3.10
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE.txt
|
||||||
|
License-File: vendor/llhttp/LICENSE
|
||||||
|
Requires-Dist: aiohappyeyeballs>=2.5.0
|
||||||
|
Requires-Dist: aiosignal>=1.4.0
|
||||||
|
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
|
||||||
|
Requires-Dist: attrs>=17.3.0
|
||||||
|
Requires-Dist: frozenlist>=1.1.1
|
||||||
|
Requires-Dist: multidict<7.0,>=4.5
|
||||||
|
Requires-Dist: propcache>=0.2.0
|
||||||
|
Requires-Dist: typing_extensions>=4.4; python_version < "3.13"
|
||||||
|
Requires-Dist: yarl<2.0,>=1.17.0
|
||||||
|
Provides-Extra: speedups
|
||||||
|
Requires-Dist: aiodns>=3.3.0; (sys_platform != "android" and sys_platform != "ios") and extra == "speedups"
|
||||||
|
Requires-Dist: Brotli>=1.2; (platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios") and extra == "speedups"
|
||||||
|
Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
|
||||||
|
Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios") and extra == "speedups"
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
==================================
|
||||||
|
Async http client/server framework
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
|
||||||
|
:height: 64px
|
||||||
|
:width: 64px
|
||||||
|
:alt: aiohttp logo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
|
||||||
|
:target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
||||||
|
:alt: GitHub Actions status for master branch
|
||||||
|
|
||||||
|
.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/aio-libs/aiohttp
|
||||||
|
:alt: codecov.io status for master branch
|
||||||
|
|
||||||
|
.. image:: https://badge.fury.io/py/aiohttp.svg
|
||||||
|
:target: https://pypi.org/project/aiohttp
|
||||||
|
:alt: Latest PyPI package version
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/dm/aiohttp
|
||||||
|
:target: https://pypistats.org/packages/aiohttp
|
||||||
|
:alt: Downloads count
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
|
||||||
|
:target: https://docs.aiohttp.org/
|
||||||
|
:alt: Latest Read The Docs
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
|
||||||
|
:target: https://codspeed.io/aio-libs/aiohttp
|
||||||
|
:alt: Codspeed.io status for aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
Key Features
|
||||||
|
============
|
||||||
|
|
||||||
|
- Supports both client and server side of HTTP protocol.
|
||||||
|
- Supports both client and server Web-Sockets out-of-the-box and avoids
|
||||||
|
Callback Hell.
|
||||||
|
- Provides Web-server with middleware and pluggable routing.
|
||||||
|
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
===============
|
||||||
|
|
||||||
|
Client
|
||||||
|
------
|
||||||
|
|
||||||
|
To get something from the web:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get('http://python.org') as response:
|
||||||
|
|
||||||
|
print("Status:", response.status)
|
||||||
|
print("Content-type:", response.headers['content-type'])
|
||||||
|
|
||||||
|
html = await response.text()
|
||||||
|
print("Body:", html[:15], "...")
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
This prints:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
Status: 200
|
||||||
|
Content-type: text/html; charset=utf-8
|
||||||
|
Body: <!doctype html> ...
|
||||||
|
|
||||||
|
Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
|
||||||
|
|
||||||
|
Server
|
||||||
|
------
|
||||||
|
|
||||||
|
An example using a simple server:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# examples/server_simple.py
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
async def handle(request):
|
||||||
|
name = request.match_info.get('name', "Anonymous")
|
||||||
|
text = "Hello, " + name
|
||||||
|
return web.Response(text=text)
|
||||||
|
|
||||||
|
async def wshandle(request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type == web.WSMsgType.text:
|
||||||
|
await ws.send_str("Hello, {}".format(msg.data))
|
||||||
|
elif msg.type == web.WSMsgType.binary:
|
||||||
|
await ws.send_bytes(msg.data)
|
||||||
|
elif msg.type == web.WSMsgType.close:
|
||||||
|
break
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
app.add_routes([web.get('/', handle),
|
||||||
|
web.get('/echo', wshandle),
|
||||||
|
web.get('/{name}', handle)])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
web.run_app(app)
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
https://aiohttp.readthedocs.io/
|
||||||
|
|
||||||
|
|
||||||
|
Demos
|
||||||
|
=====
|
||||||
|
|
||||||
|
https://github.com/aio-libs/aiohttp-demos
|
||||||
|
|
||||||
|
|
||||||
|
External links
|
||||||
|
==============
|
||||||
|
|
||||||
|
* `Third party libraries
|
||||||
|
<http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
|
||||||
|
* `Built with aiohttp
|
||||||
|
<http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
|
||||||
|
* `Powered by aiohttp
|
||||||
|
<http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
|
||||||
|
|
||||||
|
Feel free to make a Pull Request for adding your link to these pages!
|
||||||
|
|
||||||
|
|
||||||
|
Communication channels
|
||||||
|
======================
|
||||||
|
|
||||||
|
*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
|
||||||
|
|
||||||
|
*Matrix*: `#aio-libs:matrix.org <https://matrix.to/#/#aio-libs:matrix.org>`_
|
||||||
|
|
||||||
|
We support `Stack Overflow
|
||||||
|
<https://stackoverflow.com/questions/tagged/aiohttp>`_.
|
||||||
|
Please add *aiohttp* tag to your question there.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
============
|
||||||
|
|
||||||
|
- attrs_
|
||||||
|
- multidict_
|
||||||
|
- yarl_
|
||||||
|
- frozenlist_
|
||||||
|
|
||||||
|
Optionally you may install the aiodns_ library (highly recommended for sake of speed).
|
||||||
|
|
||||||
|
.. _aiodns: https://pypi.python.org/pypi/aiodns
|
||||||
|
.. _attrs: https://github.com/python-attrs/attrs
|
||||||
|
.. _multidict: https://pypi.python.org/pypi/multidict
|
||||||
|
.. _frozenlist: https://pypi.org/project/frozenlist/
|
||||||
|
.. _yarl: https://pypi.python.org/pypi/yarl
|
||||||
|
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
|
||||||
|
|
||||||
|
|
||||||
|
Keepsafe
|
||||||
|
========
|
||||||
|
|
||||||
|
The aiohttp community would like to thank Keepsafe
|
||||||
|
(https://www.getkeepsafe.com) for its support in the early days of
|
||||||
|
the project.
|
||||||
|
|
||||||
|
|
||||||
|
Source code
|
||||||
|
===========
|
||||||
|
|
||||||
|
The latest developer version is available in a GitHub repository:
|
||||||
|
https://github.com/aio-libs/aiohttp
|
||||||
|
|
||||||
|
Benchmarks
|
||||||
|
==========
|
||||||
|
|
||||||
|
If you are interested in efficiency, the AsyncIO community maintains a
|
||||||
|
list of benchmarks on the official wiki:
|
||||||
|
https://github.com/python/asyncio/wiki/Benchmarks
|
||||||
|
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
||||||
|
:target: https://matrix.to/#/%23aio-libs:matrix.org
|
||||||
|
:alt: Matrix Room — #aio-libs:matrix.org
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
||||||
|
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
|
||||||
|
:alt: Matrix Space — #aio-libs-space:matrix.org
|
||||||
|
|
||||||
|
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
|
||||||
|
:target: https://insights.linuxfoundation.org/project/aiohttp
|
||||||
|
:alt: LFX Health Score
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
aiohttp-3.14.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
aiohttp-3.14.1.dist-info/METADATA,sha256=lSrt4tDtmd7yhoDSTEq0NNoU8cSDjfDJAuOCCHWKil0,8262
|
||||||
|
aiohttp-3.14.1.dist-info/RECORD,,
|
||||||
|
aiohttp-3.14.1.dist-info/WHEEL,sha256=Tc3fF66yn9Kh-hkUUsdKQyuB9Lw0CDoeANnEbSVc3f4,190
|
||||||
|
aiohttp-3.14.1.dist-info/licenses/LICENSE.txt,sha256=Lkvl_GxMcqRm_LZl1ybgSaaJGYH-U2xPBLY2Z0lGHSM,11313
|
||||||
|
aiohttp-3.14.1.dist-info/licenses/vendor/llhttp/LICENSE,sha256=YoFo1ou1qKF-C777O9dOMm4e3CBYPXb1L0V8gskhhn0,1069
|
||||||
|
aiohttp-3.14.1.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
|
||||||
|
aiohttp/.hash/_cparser.pxd.hash,sha256=1xYKvgB1DahaZj6GMSLJ9qi5KmoJDD8ZPp3CIxah0ig,121
|
||||||
|
aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125
|
||||||
|
aiohttp/.hash/_http_parser.pyx.hash,sha256=D15N0PIazROldZ2ezxeuuu4VuQP9OxrN8sutpdcL6TA,125
|
||||||
|
aiohttp/.hash/_http_writer.pyx.hash,sha256=eqqhtJepEtpikWbd_gUoZOWyEBUwmHCd1fq2jfIrXSg,125
|
||||||
|
aiohttp/.hash/hdrs.py.hash,sha256=cBtTPqXxWpYN--0v2ukVWjjfVbnT3Csii1PkBJBCz8E,116
|
||||||
|
aiohttp/__init__.py,sha256=1dDCbOmlrF1FnB_LRnU-jVKY1XngQ9guX1ZasoTH-TY,8339
|
||||||
|
aiohttp/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/_cookie_helpers.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/abc.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/base_protocol.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client_exceptions.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client_middleware_digest_auth.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client_middlewares.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client_proto.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client_reqrep.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/client_ws.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/compression_utils.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/connector.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/cookiejar.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/formdata.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/hdrs.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/helpers.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/http.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/http_exceptions.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/http_parser.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/http_websocket.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/http_writer.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/log.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/multipart.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/payload.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/payload_streamer.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/pytest_plugin.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/resolver.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/streams.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/tcp_helpers.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/test_utils.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/tracing.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/typedefs.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_app.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_exceptions.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_fileresponse.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_log.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_middlewares.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_protocol.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_request.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_response.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_routedef.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_runner.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_server.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_urldispatcher.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/web_ws.cpython-312.pyc,,
|
||||||
|
aiohttp/__pycache__/worker.cpython-312.pyc,,
|
||||||
|
aiohttp/_cookie_helpers.py,sha256=uOe8v5yx9N9N2JHFJiQOjuN2SiIV7D1lxnTfrXc6oEU,14092
|
||||||
|
aiohttp/_cparser.pxd,sha256=nvnwTkOaMAIazM-OUB6H66LpsNNeUG3nFxbSSLQoQEM,4336
|
||||||
|
aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68
|
||||||
|
aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007
|
||||||
|
aiohttp/_http_parser.cpython-312-x86_64-linux-gnu.so,sha256=3gTnbWntd2VXGfBrHeUH3BYFoLUC_2n0WJZZ76wPR2Q,2971648
|
||||||
|
aiohttp/_http_parser.pyx,sha256=mNLUenKTRZkNlXXc9oxVDeXVJ31gsrI4XR69jrDWxPw,33860
|
||||||
|
aiohttp/_http_writer.cpython-312-x86_64-linux-gnu.so,sha256=FaMRMxE6_VwOwd6dPNhRn_7JMDsvad3qvrZNvR-OrdA,474400
|
||||||
|
aiohttp/_http_writer.pyx,sha256=FmhqRLv-qyvL2BJvVwk128VFqwYMb1b1vHLPCt3awXA,4823
|
||||||
|
aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128
|
||||||
|
aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128
|
||||||
|
aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=DM51AsKpHR3Bl2EIx0eUSy1uvusTDfylNeRCMT66erk,132
|
||||||
|
aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44
|
||||||
|
aiohttp/_websocket/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/__pycache__/helpers.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/__pycache__/models.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/__pycache__/reader.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/__pycache__/reader_c.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/__pycache__/reader_py.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/__pycache__/writer.cpython-312.pyc,,
|
||||||
|
aiohttp/_websocket/helpers.py,sha256=lMDbpiZefmHTGNxjpe6K09ZlF048KxzcOvbh-ZYqvg0,5026
|
||||||
|
aiohttp/_websocket/mask.cpython-312-x86_64-linux-gnu.so,sha256=phpHU3AXOq0nAtNgxac3ZtW2sxD-re_5ZeAl5V8lNgc,253760
|
||||||
|
aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112
|
||||||
|
aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397
|
||||||
|
aiohttp/_websocket/models.py,sha256=gJLskxkUKakYFsVMdjOclSjt41rQWRCL4RfRB_wmwO0,2952
|
||||||
|
aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030
|
||||||
|
aiohttp/_websocket/reader_c.cpython-312-x86_64-linux-gnu.so,sha256=Zk-kBzozgh0cifrVwO-ZUVXRJstKqbX7_9lhy9qC_R4,1789112
|
||||||
|
aiohttp/_websocket/reader_c.pxd,sha256=l-ODGpJpOx4FxpsCtkRyITmmRvBlRo8mv87qNgeQZbo,2683
|
||||||
|
aiohttp/_websocket/reader_c.py,sha256=vf7Y6JB6hpLE17OQ39IhO70ga_DXFYksfv45M5n1wOU,20073
|
||||||
|
aiohttp/_websocket/reader_py.py,sha256=vf7Y6JB6hpLE17OQ39IhO70ga_DXFYksfv45M5n1wOU,20073
|
||||||
|
aiohttp/_websocket/writer.py,sha256=JA4ejOQMCvAvIXe_0f6uRx_aGSO4-zVUc1HJBZ125ak,11259
|
||||||
|
aiohttp/abc.py,sha256=KvpM3wDEWl_iIdNQX3NEnJf3pYAtgxynAvp5s4JkNrU,7536
|
||||||
|
aiohttp/base_protocol.py,sha256=bL6dSlNacbmln20Plez_gqYXTcFeg-V1hkHVDk4s8vU,4688
|
||||||
|
aiohttp/client.py,sha256=PzaLAKgPMytcA75z1ZIEjTBzTWR3U7wT66f38OKTwmI,64312
|
||||||
|
aiohttp/client_exceptions.py,sha256=XvJ57ppYs2IubcY3efBUkSpPElwQDqt3SWU_yssvjyI,11609
|
||||||
|
aiohttp/client_middleware_digest_auth.py,sha256=Z8ufMnBF8RQ_JaVfG3bUbAoRua_kC7RSIexQM2zm9wQ,19042
|
||||||
|
aiohttp/client_middlewares.py,sha256=kP5N9CMzQPMGPIEydeVUiLUTLsw8Vl8Gr4qAWYdu3vM,1918
|
||||||
|
aiohttp/client_proto.py,sha256=u6AZU05mZxAFG4saTaxn5lVN9vy4cmgvF9k7HQG260A,12633
|
||||||
|
aiohttp/client_reqrep.py,sha256=RWyXzuIi0NETNeuNADMbWPGd-mxcpVLsVQM0N582pU8,54704
|
||||||
|
aiohttp/client_ws.py,sha256=PiMLbWN6IBEXGiWuqL1KyQqIAydDGsWMitX_pd8cjLo,19468
|
||||||
|
aiohttp/compression_utils.py,sha256=_HTGK9dZOojGd4kyrk2A5yAt1pzVyKJh4AH5eVH0DQ8,15840
|
||||||
|
aiohttp/connector.py,sha256=0iYWUWjhc8zDYwWd-J4mtfhjojzGRRcbnQbnuwY-njU,70036
|
||||||
|
aiohttp/cookiejar.py,sha256=Y80b6_hSiDi4lN0iOb3X5983VONf-i4GhRH447gzTr0,25815
|
||||||
|
aiohttp/formdata.py,sha256=zRWW2ODYkWeahDOd-hwra-__EUizdB2bf6hAMgkCKhc,6514
|
||||||
|
aiohttp/hdrs.py,sha256=pGrWw6L6-NJqLGr8GiIQzjeaI_J5n857JqAfbOWkBkI,5106
|
||||||
|
aiohttp/helpers.py,sha256=dSbEmGl8XsLU2zwmD0dyp4wHHvHrH5TFaofkSH-OmsA,33017
|
||||||
|
aiohttp/http.py,sha256=CHXxFtOXK85RNijYkfpjvjCbKlJc3hIGRsCFAnh83hk,2077
|
||||||
|
aiohttp/http_exceptions.py,sha256=I1nyopt1sYV0GqpW2ra4pnCmFcOqMoDC4dELghEk2lI,3115
|
||||||
|
aiohttp/http_parser.py,sha256=nrXK1evVj04TjpbOu4tOx4w0yUlzG2PaEFxq0wvmfNY,45170
|
||||||
|
aiohttp/http_websocket.py,sha256=8jVNshtgNPSDnab0Dc1lAO7HFvtT7y79eZKjUXX6e28,983
|
||||||
|
aiohttp/http_writer.py,sha256=bXHXXWkEnySdYX3cCmYCFgNFsnxwPNCs-0nMjSPDRAo,12602
|
||||||
|
aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325
|
||||||
|
aiohttp/multipart.py,sha256=CJB4szZ8Zhv4htswXvSDfwY8HjlVGZFyvzGXIYo7b50,44179
|
||||||
|
aiohttp/payload.py,sha256=MXcl_K0gcblkN_YFXE_Up6N7IQwjH8uM4HTfn9Axbmg,41480
|
||||||
|
aiohttp/payload_streamer.py,sha256=PGqJKt_1ca_7i6p1UgU8jrpO1toOt5EQDQMOK9LjtV4,2225
|
||||||
|
aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
|
||||||
|
aiohttp/pytest_plugin.py,sha256=eGhRoy0TKNOQ3mtabkpoMgt0JThnVDr406h_kyZkQe8,12976
|
||||||
|
aiohttp/resolver.py,sha256=jmrAUnhayFdSjTsCt2_coosdVmc5RgEB5Agr65Q0sQk,9954
|
||||||
|
aiohttp/streams.py,sha256=k-XDoAfb3P8t5sHvWyEONXzYHoZ4JmJYr_aZB9CQF6c,24127
|
||||||
|
aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961
|
||||||
|
aiohttp/test_utils.py,sha256=RqqoQTTI4VpZDvOcLMmQgxisuRMCIX0qOjG019EtEpM,24636
|
||||||
|
aiohttp/tracing.py,sha256=4mokNTKdX5YuoSrvVnG4D7WTPMwe-UlYHVYxQjRv4Fw,14500
|
||||||
|
aiohttp/typedefs.py,sha256=I-V6S4T73fI_wkJfBYiL7otEeRvVHmSdsk-YYPR6Fj0,1672
|
||||||
|
aiohttp/web.py,sha256=gsyxRSPWV3iZjPUbOR3dIGcp1jm2ltBtXwkPh91co3Q,18426
|
||||||
|
aiohttp/web_app.py,sha256=hmT_DWXKwHMqmFBLE8JHhevLaaTST73ikovIIx_Tw6k,19379
|
||||||
|
aiohttp/web_exceptions.py,sha256=CsEG88dQINYSFpklCE_7bZgL1IVjHoy5dlTSVQGHPRQ,10354
|
||||||
|
aiohttp/web_fileresponse.py,sha256=5vlf-OmfgFxG5O1zGrzk-SPRkbs58izLlbQldT-DHXE,16404
|
||||||
|
aiohttp/web_log.py,sha256=lJCDsZ2xJp7HqSuijaTZyqxJ0oNWPhwxJE1FVGUWwWU,8487
|
||||||
|
aiohttp/web_middlewares.py,sha256=OIfnnCHJgRnKjzDjHoc_VzNcLuVf9y8kv5aTzM2VGvc,4152
|
||||||
|
aiohttp/web_protocol.py,sha256=RyGK7u4Lpl8yC2rSbHcKiYhSDbKSbHuA-GZTaN8q-RY,31092
|
||||||
|
aiohttp/web_request.py,sha256=o99Os7_M_C4poSc58el9q44VGO6VctJaJMFKhi7F6R8,31867
|
||||||
|
aiohttp/web_response.py,sha256=7EjxpTW4a6du5X0utXenUbSO6fbY_MgfINppJUEre-M,30783
|
||||||
|
aiohttp/web_routedef.py,sha256=S_uRfJ87LCM_fFTAIrqYTWiIqDAk0pLLYdUr1vGU9BM,6057
|
||||||
|
aiohttp/web_runner.py,sha256=PGUdZs_d2qzM3Tnb6yCQQoxfwqQZFuVr_NGIJBZq9Ms,12708
|
||||||
|
aiohttp/web_server.py,sha256=LawT47SJuLSIYgkAWL_haFgH6lVyuoD3zyFTvBatvsE,3170
|
||||||
|
aiohttp/web_urldispatcher.py,sha256=lc9Cou6tP8YJTJS2MEQWVcJV9C5dFKTyhdCLjLtkOzU,43893
|
||||||
|
aiohttp/web_ws.py,sha256=fqE-c95dxvwkZfm8M-vKeP5pCteHCmrhAdI-1o4J348,27455
|
||||||
|
aiohttp/worker.py,sha256=osuMCad7kBmL7FeR6EEEdsfkDqOaBDYvAzHMgW_hsIc,8506
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (82.0.1)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||||
|
Tag: cp312-cp312-manylinux2014_x86_64
|
||||||
|
Tag: cp312-cp312-manylinux_2_28_x86_64
|
||||||
|
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright aio-libs contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright © 2018 Fedor Indutny
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||||
|
persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||||
|
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@ -0,0 +1 @@
|
|||||||
|
aiohttp
|
||||||
@ -0,0 +1 @@
|
|||||||
|
9ef9f04e439a30021acccf8e501e87eba2e9b0d35e506de71716d248b4284043 /home/runner/work/aiohttp/aiohttp/aiohttp/_cparser.pxd
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user