Spaces:
Running
Running
Restore main.py: fix test-telegram probe + 30s timeout
Browse files
main.py
CHANGED
|
@@ -1 +1,451 @@
|
|
| 1 |
-
IiIiVHJhZGUgQ29waWxvdCDigJQgRmFzdEFQSSBzZXJ2ZXIuCgpFbmRwb2ludHM6CiAgR0VUIC8gICAgICAgICAgICDihpIgZGFzaGJvYXJkIEhUTUwgKHNlcnZlZCBmcm9tIHN0YXRpYy9pbmRleC5odG1sKQogIEdFVCAvYXBpL3NjYW4gICAg4oaSIHJhbmtlZCBzaG9ydGxpc3QgSlNPTgogIEdFVCAvYXBpL2NhcmQve3N5bWJvbH0g4oaSIGZ1bGwgdHJhZGUgY2FyZCBmb3Igb25lIHN5bWJvbAogIEdFVCAvYXBpL3N0YXR1cyAg4oaSIGhlYWx0aCArIGFjdGl2ZSBzb3VyY2UKCkFsbCBjb21wdXRhdGlvbiBpcyBkZXRlcm1pbmlzdGljIOKAlCBubyBBSSBpbiB0aGUgZGF0YSBwYXRoLgoiIiIKZnJvbSBfX2Z1dHVyZV9fIGltcG9ydCBhbm5vdGF0aW9ucwppbXBvcnQgbWF0aCwgc3RhdGlzdGljcywgdGltZSwgb3MKZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCmZyb20gZnVuY3Rvb2xzIGltcG9ydCBscnVfY2FjaGUKZnJvbSBkYXRldGltZSBpbXBvcnQgZGF0ZXRpbWUsIHRpbWV6b25lCgppbXBvcnQgYXN5bmNpbwppbXBvcnQgbG9nZ2luZwpmcm9tIGNvbnRleHRsaWIgaW1wb3J0IGFzeW5jY29udGV4dG1hbmFnZXIKCmZyb20gZmFzdGFwaSBpbXBvcnQgRmFzdEFQSSwgSFRUUEV4Y2VwdGlvbiwgUXVlcnkKZnJvbSBmYXN0YXBpLnN0YXRpY2ZpbGVzIGltcG9ydCBTdGF0aWNGaWxlcwpmcm9tIGZhc3RhcGkucmVzcG9uc2VzIGltcG9ydCBKU09OUmVzcG9uc2UsIEZpbGVSZXNwb25zZQoKZnJvbSBleGNoYW5nZSBpbXBvcnQgcGlja19zb3VyY2UsIFNPVVJDRVMsIEJpbmdYLCBCaW5hbmNlCmZyb20gc2NvcmVyIGltcG9ydCBzY29yZV9zeW1ib2wKZnJvbSBjYXRhbHlzdCBpbXBvcnQgZmV0Y2hfZmVhcl9ncmVlZCwgZmV0Y2hfY29pbl9uZXdzLCBzY29yZV9jYXRhbHlzdCwgZmV0Y2hfY3J5cHRvX2hlYWRsaW5lcwpmcm9tIHNpZ25hbHMgaW1wb3J0IHNjYW5fdW5pdmVyc2UsIGJ1aWxkX3NpZ25hbF91bml2ZXJzZQpmcm9tIGJhY2t0ZXN0IGltcG9ydCBiYWNrdGVzdF9zeW1ib2wKZnJvbSBzaWduYWxfbG9nIGltcG9ydCBsb2dfc2lnbmFsLCByZXNvbHZlX3NpZ25hbHMsIGdldF9hY2N1cmFjeSwgZ2V0X3N5bWJvbF9hY2N1cmFjeQoKIyBDcnlwdG9QYW5pYyB0b2tlbiDigJQgc2V0IGluIEhGIFNwYWNlcyBTZWNyZXRzIGFzIENSWVBUT1BBTklDX1RPS0VOCiMgTmV2ZXIgaGFyZGNvZGUgb3IgbG9nIHRoaXMgdmFsdWUKQ1JZUFRPUEFOSUNfVE9LRU4gPSBvcy5lbnZpcm9uLmdldCgiQ1JZUFRPUEFOSUNfVE9LRU4iLCAiIikKCmxvZ2dlciA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKCJtYWluIikKCkFMRVJUX0lOVEVSVkFMID0gMzAwICAjIDUgbWludXRlcwoKYXN5bmMgZGVmIF9hbGVydF9sb29wKCk6CiAgICAiIiJCYWNrZ3JvdW5kIHRhc2s6IHNjYW4gZXZlcnkgNSBtaW4gYW5kIGZpcmUgVGVsZWdyYW0gYWxlcnRzIGZvciBoaWdoLWNvbmZpZGVuY2UgY2FyZHMuIiIiCiAgICBmcm9tIHRlbGVncmFtX2FsZXJ0IGltcG9ydCBzZW5kX2FsZXJ0CiAgICAjIFJlYWQgYWNjb3VudCBzaXplIGZyb20gSEYgU2VjcmV0IOKAlCBkZWZhdWx0cyB0byAxMCBpZiBub3Qgc2V0CiAgICB0cnk6CiAgICAgICAgYWxlcnRfYWNjb3VudCA9IGZsb2F0KG9zLmVudmlyb24uZ2V0KCJBTEVSVF9BQ0NPVU5UIiwgIjEwIikpCiAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICBhbGVydF9hY2NvdW50ID0gMTAuMAogICAgbG9nZ2VyLmluZm8oIkFsZXJ0IGxvb3Agc3RhcnRpbmcg4oCUIGFjY291bnQ9JCUuMmYiLCBhbGVydF9hY2NvdW50KQoKICAgIGF3YWl0IGFzeW5jaW8uc2xlZXAoNjApICAjIGdpdmUgYXBwIHRpbWUgdG8gZnVsbHkgYm9vdCBiZWZvcmUgZmlyc3Qgc2NhbgogICAgd2hpbGUgVHJ1ZToKICAgICAgICB0cnk6CiAgICAgICAgICAgIHNyYyA9IGdldF9zb3VyY2UoKQogICAgICAgICAgICBzZWNvbmRhcnkgPSBnZXRfc2Vjb25kYXJ5KCkKICAgICAgICAgICAgY2FuZGlkYXRlcyA9IHJhbmtfdW5pdmVyc2Uoc3JjLCBzcmMubWluX3ZvbCwgMzApCiAgICAgICAgICAgIGZvciBjIGluIGNhbmRpZGF0ZXM6CiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgY2FyZCA9IHNjb3JlX3N5bWJvbChzcmMsIGNbInN5bWJvbCJdLCBhbGVydF9hY2NvdW50LCAxLjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWNvbmRhcnk9c2Vjb25kYXJ5LCB1c2VyX2xldmVyYWdlPTYuMCkKICAgICAgICAgICAgICAgICAgICBjb25mICAgPSBjYXJkLmdldCgiY29uZmlkZW5jZSIsIDApCiAgICAgICAgICAgICAgICAgICAgdmV0b2VkID0gY2FyZC5nZXQoInZldG9lZCIsIEZhbHNlKQogICAgICAgICAgICAgICAgICAgIGlmIGNvbmYgPj0gOC44IGFuZCBub3QgdmV0b2VkOgogICAgICAgICAgICAgICAgICAgICAgICB0dl9zeW0gPSBzcmMudHZfc3ltYm9sKGNhcmRbInN5bWJvbCJdKQogICAgICAgICAgICAgICAgICAgICAgICBhd2FpdCBzZW5kX2FsZXJ0KGNhcmQsIHR2X3N5bSkKICAgICAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXN5bmNpby5zbGVlcCgxKSAgICMgc21hbGwgZ2FwIGJldHdlZW4gc2VuZHMKICAgICAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgICAgICAgICBsb2dnZXIuZGVidWcoIkFsZXJ0IHNjYW4gZXJyb3IgZm9yICVzOiAlcyIsIGMuZ2V0KCJzeW1ib2wiKSwgZSkKICAgICAgICAgICAgICAgIGF3YWl0IGFzeW5jaW8uc2xlZXAoMC41KSAgIyBnZW50bGUgcmF0ZSBsaW1pdCBiZXR3ZWVuIHN5bWJvbHMKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIGxvZ2dlci5lcnJvcigiQWxlcnQgbG9vcCBlcnJvcjogJXMiLCBlKQogICAgICAgIGF3YWl0IGFzeW5jaW8uc2xlZXAoQUxFUlRfSU5URVJWQUwpCgoKIyBFdmVudCBsb29wIHJlZmVyZW5jZSDigJQgY2FwdHVyZWQgYXQgc3RhcnR1cCBzbyBzeW5jIGVuZHBvaW50cyBjYW4gc2NoZWR1bGUgY29yb3V0aW5lcwpfZXZlbnRfbG9vcDogYXN5bmNpby5BYnN0cmFjdEV2ZW50TG9vcCB8IE5vbmUgPSBOb25lCgoKQGFzeW5jY29udGV4dG1hbmFnZXIKYXN5bmMgZGVmIGxpZmVzcGFuKGFwcCk6CiAgICBnbG9iYWwgX2V2ZW50X2xvb3AKICAgIF9ldmVudF9sb29wID0gYXN5bmNpby5nZXRfcnVubmluZ19sb29wKCkKICAgIHRhc2sgPSBhc3luY2lvLmNyZWF0ZV90YXNrKF9hbGVydF9sb29wKCkpCiAgICB0cnk6CiAgICAgICAgeWllbGQKICAgIGZpbmFsbHk6CiAgICAgICAgdGFzay5jYW5jZWwoKQogICAgICAgIHRyeToKICAgICAgICAgICAgYXdhaXQgdGFzawogICAgICAgIGV4Y2VwdCBhc3luY2lvLkNhbmNlbGxlZEVycm9yOgogICAgICAgICAgICBwYXNzCgoKYXBwID0gRmFzdEFQSSh0aXRsZT0iVHJhZGUgQ29waWxvdCIsIHZlcnNpb249IjEuMCIsIGxpZmVzcGFuPWxpZmVzcGFuKQoKIyBDYWNoZSB0aGUgYWN0aXZlIHNvdXJjZSBmb3IgNSBtaW4gKGF2b2lkcyByZS1waW5naW5nIG9uIGV2ZXJ5IHJlcXVlc3QpCl9zb3VyY2VfY2FjaGU6IGRpY3QgPSB7fQpfc291cmNlX3RzOiBmbG9hdCA9IDAKU09VUkNFX1RUTCA9IDMwMAoKIyBTZWNvbmRhcnkgc291cmNlIGZvciBjcm9zcy12YWxpZGF0aW9uIChCaW5hbmNlIOKAlCBkZWVwZXIgT0kvZnVuZGluZyBkYXRhKQpfYmluYW5jZTogQmluYW5jZSB8IE5vbmUgPSBOb25lCl9iaW5hbmNlX29rOiBib29sID0gRmFsc2UKX2JpbmFuY2VfdHM6IGZsb2F0ID0gMApCSU5BTkNFX1RUTCA9IDMwMAoKCmRlZiBnZXRfc291cmNlKCk6CiAgICBnbG9iYWwgX3NvdXJjZV9jYWNoZSwgX3NvdXJjZV90cwogICAgaWYgdGltZS50aW1lKCkgLSBfc291cmNlX3RzID4gU09VUkNFX1RUTCBvciBub3QgX3NvdXJjZV9jYWNoZS5nZXQoInNyYyIpOgogICAgICAgIHNyYyA9IHBpY2tfc291cmNlKCkKICAgICAgICBfc291cmNlX2NhY2hlID0geyJzcmMiOiBzcmN9CiAgICAgICAgX3NvdXJjZV90cyA9IHRpbWUudGltZSgpCiAgICByZXR1cm4gX3NvdXJjZV9jYWNoZVsic3JjIl0KCgpkZWYgZ2V0X3NlY29uZGFyeSgpOgogICAgIiIiUmV0dXJucyBCaW5hbmNlIGluc3RhbmNlIGlmIHJlYWNoYWJsZSwgZWxzZSBOb25lLiBVc2VkIGZvciBjcm9zcy12YWxpZGF0aW9uLiIiIgogICAgZ2xvYmFsIF9iaW5hbmNlLCBfYmluYW5jZV9vaywgX2JpbmFuY2VfdHMKICAgIGlmIHRpbWUudGltZSgpIC0gX2JpbmFuY2VfdHMgPCBCSU5BTkNFX1RUTDoKICAgICAgICByZXR1cm4gX2JpbmFuY2UgaWYgX2JpbmFuY2Vfb2sgZWxzZSBOb25lCiAgICB0cnk6CiAgICAgICAgYiA9IEJpbmFuY2UoKQogICAgICAgIGIucGluZygpCiAgICAgICAgX2JpbmFuY2UgPSBiCiAgICAgICAgX2JpbmFuY2Vfb2sgPSBUcnVlCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIF9iaW5hbmNlID0gTm9uZQogICAgICAgIF9iaW5hbmNlX29rID0gRmFsc2UKICAgIF9iaW5hbmNlX3RzID0gdGltZS50aW1lKCkKICAgIHJldHVybiBfYmluYW5jZSBpZiBfYmluYW5jZV9vayBlbHNlIE5vbmUKCgpkZWYgcmFua191bml2ZXJzZShzcmMsIG1pbl92b2x1bWU6IGZsb2F0LCB0b3BfbjogaW50KSAtPiBsaXN0W2RpY3RdOgogICAgcGVycHMgPSBzcmMudW5pdmVyc2UoKQogICAgcm93cyA9IFtdCiAgICBmb3IgdCBpbiBzcmMudGlja2VycygpOgogICAgICAgIGlmIHRbInN5bWJvbCJdIG5vdCBpbiBwZXJwczoKICAgICAgICAgICAgY29udGludWUKICAgICAgICBxdiA9IHRbInF1b3RlX3ZvbHVtZV91c2QiXQogICAgICAgIGxvLCBoaSA9IHQuZ2V0KCJsb3ciLCAwKSwgdC5nZXQoImhpZ2giLCAwKQogICAgICAgIGlmIHF2IDwgbWluX3ZvbHVtZSBvciBsbyA8PSAwOgogICAgICAgICAgICBjb250aW51ZQogICAgICAgIHRbInJhbmdlXzI0aF9wY3QiXSA9IChoaSAtIGxvKSAvIGxvICogMTAwCiAgICAgICAgcm93cy5hcHBlbmQodCkKICAgIGlmIGxlbihyb3dzKSA8IDM6CiAgICAgICAgcmV0dXJuIFt7InN5bWJvbCI6IHN9IGZvciBzIGluIHNyYy5hbmNob3JzXQoKICAgIGxvZ192b2xzID0gW21hdGgubG9nKHJbInF1b3RlX3ZvbHVtZV91c2QiXSkgZm9yIHIgaW4gcm93c10KICAgIHJhbmdlcyA9IFtyWyJyYW5nZV8yNGhfcGN0Il0gZm9yIHIgaW4gcm93c10KICAgIG12LCBzdiA9IHN0YXRpc3RpY3MubWVhbihsb2dfdm9scyksIHN0YXRpc3RpY3Muc3RkZXYobG9nX3ZvbHMpCiAgICBtciwgc3IgPSBzdGF0aXN0aWNzLm1lYW4ocmFuZ2VzKSwgc3RhdGlzdGljcy5zdGRldihyYW5nZXMpCiAgICBmb3IgciwgbHYsIHJnIGluIHppcChyb3dzLCBsb2dfdm9scywgcmFuZ2VzKToKICAgICAgICByWyJzY2FuX3Njb3JlIl0gPSByb3VuZCgwLjUgKiAobHYgLSBtdikgLyBzdiArIDAuNSAqIChyZyAtIG1yKSAvIHNyLCAzKQoKICAgIHJvd3Muc29ydChrZXk9bGFtYmRhIHI6IHJbInNjYW5fc2NvcmUiXSwgcmV2ZXJzZT1UcnVlKQogICAgcGlja2VkID0gcm93c1s6dG9wX25dCiAgICBwaWNrZWRfc3ltcyA9IHtyWyJzeW1ib2wiXSBmb3IgciBpbiBwaWNrZWR9CiAgICBmb3Igc3ltIGluIHNyYy5hbmNob3JzOgogICAgICAgIGlmIHN5bSBub3QgaW4gcGlja2VkX3N5bXM6CiAgICAgICAgICAgIGV4dHJhID0gbmV4dCgociBmb3IgciBpbiByb3dzIGlmIHJbInN5bWJvbCJdID09IHN5bSksIE5vbmUpCiAgICAgICAgICAgIGlmIGV4dHJhOgogICAgICAgICAgICAgICAgZXh0cmFbImFuY2hvciJdID0gVHJ1ZQogICAgICAgICAgICAgICAgcGlja2VkLmFwcGVuZChleHRyYSkKICAgIHJldHVybiBwaWNrZWQKCgojIOKUgOKUgCBBUEkgcm91dGVzIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKQGFwcC5nZXQoIi9hcGkvc3RhdHVzIikKZGVmIHN0YXR1cygpOgogICAgdHJ5OgogICAgICAgIHNyYyA9IGdldF9zb3VyY2UoKQogICAgICAgIHJldHVybiB7Im9rIjogVHJ1ZSwgInNvdXJjZSI6IHNyYy5uYW1lLCAibWFya2V0Ijogc3JjLm1hcmtldF9ub3RlLAogICAgICAgICAgICAgICAgInRzIjogZGF0ZXRpbWUubm93KHRpbWV6b25lLnV0YykuaXNvZm9ybWF0KCl9CiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmV0dXJuIEpTT05SZXNwb25zZSh7Im9rIjogRmFsc2UsICJlcnJvciI6IHN0cihlKX0sIHN0YXR1c19jb2RlPTUwMykKCgpAYXBwLmdldCgiL2FwaS9zY2FuIikKZGVmIHNjYW4odG9wOiBpbnQgPSA4LCBtaW5fdm9sdW1lOiBmbG9hdCA9IE5vbmUsIGFjY291bnQ6IGZsb2F0ID0gMTAwMDAwLAogICAgICAgICByaXNrX3BjdDogZmxvYXQgPSAxLjAsCiAgICAgICAgIHVzZXJfbGV2ZXJhZ2U6IGZsb2F0ID0gUXVlcnkoZGVmYXVsdD02LjAsIGdlPTEuMCwgbGU9MTAwLjApKToKICAgIHRyeToKICAgICAgICBzcmMgPSBnZXRfc291cmNlKCkKICAgICAgICBmbG9vciA9IG1pbl92b2x1bWUgb3Igc3JjLm1pbl92b2wKICAgICAgICBjYW5kaWRhdGVzID0gcmFua191bml2ZXJzZShzcmMsIGZsb29yLCB0b3ApCiAgICAgICAgc2Vjb25kYXJ5ID0gZ2V0X3NlY29uZGFyeSgpCiAgICAgICAgY2FyZHMgPSBbXQogICAgICAgIGZvciBjIGluIGNhbmRpZGF0ZXM6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIGNhcmQgPSBzY29yZV9zeW1ib2woc3JjLCBjWyJzeW1ib2wiXSwgYWNjb3VudCwgcmlza19wY3QsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlY29uZGFyeT1zZWNvbmRhcnksIHVzZXJfbGV2ZXJhZ2U9dXNlcl9sZXZlcmFnZSkKICAgICAgICAgICAgICAgIGNhcmRbInJhbmtfaW5mbyJdID0ge2s6IGMuZ2V0KGspIGZvciBrIGluCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAoInNjYW5fc2NvcmUiLCJjaGFuZ2VfMjRoX3BjdCIsInJhbmdlXzI0aF9wY3QiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJxdW90ZV92b2x1bWVfdXNkIiwiYW5jaG9yIil9CiAgICAgICAgICAgICAgICBjYXJkcy5hcHBlbmQoY2FyZCkKICAgICAgICAgICAgICAgICMgQXV0by1sb2cgZXZlcnkgbm9uLXZldG9lZCBzaWduYWwgZm9yIGxpdmUgYWNjdXJhY3kgdHJhY2tpbmcKICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBsb2dfc2lnbmFsKGNhcmQpCiAgICAgICAgICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICAgICAgICAgIHBhc3MKICAgICAgICAgICAgICAgICMgRmlyZSBUZWxlZ3JhbSBhbGVydCBpZiBjb25maWRlbmNlID49IDkuMAogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIGNvbmYgICA9IGNhcmQuZ2V0KCJjb25maWRlbmNlIiwgMCkKICAgICAgICAgICAgICAgICAgICB2ZXRvZWQgPSBjYXJkLmdldCgidmV0b2VkIiwgRmFsc2UpCiAgICAgICAgICAgICAgICAgICAgaWYgY29uZiA+PSA4LjggYW5kIG5vdCB2ZXRvZWQgYW5kIF9ldmVudF9sb29wIGlzIG5vdCBOb25lOgogICAgICAgICAgICAgICAgICAgICAgICBmcm9tIHRlbGVncmFtX2FsZXJ0IGltcG9ydCBzZW5kX2FsZXJ0CiAgICAgICAgICAgICAgICAgICAgICAgIHR2X3N5bSA9IHNyYy50dl9zeW1ib2woY2FyZFsic3ltYm9sIl0pCiAgICAgICAgICAgICAgICAgICAgICAgIGFzeW5jaW8ucnVuX2Nvcm91dGluZV90aHJlYWRzYWZlKAogICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VuZF9hbGVydChjYXJkLCB0dl9zeW0sIG1hbnVhbD1UcnVlKSwgX2V2ZW50X2xvb3AKICAgICAgICAgICAgICAgICAgICAgICAgKQogICAgICAgICAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgICAgICAgICB0aW1lLnNsZWVwKDAuMDUpCiAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgICAgIGNhcmRzLmFwcGVuZCh7InN5bWJvbCI6IGNbInN5bWJvbCJdLCAiZXJyb3IiOiBzdHIoZSlbOjEyMF19KQogICAgICAgIGNhcmRzLnNvcnQoa2V5PWxhbWJkYSBjOiBjLmdldCgiY29uZmlkZW5jZSIsIDApLCByZXZlcnNlPVRydWUpCiAgICAgICAgcmV0dXJuIHsiZmV0Y2hlZF9hdCI6IGRhdGV0aW1lLm5vdyh0aW1lem9uZS51dGMpLmlzb2Zvcm1hdCgpLAogICAgICAgICAgICAgICAgInNvdXJjZSI6IHNyYy5uYW1lLCAibWFya2V0Ijogc3JjLm1hcmtldF9ub3RlLCAiY2FyZHMiOiBjYXJkc30KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByYWlzZSBIVFRQRXhjZXB0aW9uKDUwMywgc3RyKGUpKQoKCkBhcHAuZ2V0KCIvYXBpL3ByaWNlcyIpCmRlZiBwcmljZXMoc3ltYm9sczogc3RyID0gIiIpOgogICAgIiIiTGlnaHR3ZWlnaHQgcHJpY2UgdGlja2VyIOKAlCByZXR1cm5zIGN1cnJlbnQgcHJpY2UgKyAyNGggY2hhbmdlIGZvciBhCiAgICBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBzeW1ib2xzLiBVc2VkIGZvciByZWFsLXRpbWUgY2FyZCBwcmljZSB1cGRhdGVzLiIiIgogICAgdHJ5OgogICAgICAgIHNyYyA9IGdldF9zb3VyY2UoKQogICAgICAgIHJlcXVlc3RlZCA9IHtzLnN0cmlwKCkgZm9yIHMgaW4gc3ltYm9scy5zcGxpdCgiLCIpIGlmIHMuc3RyaXAoKX0KICAgICAgICB0aWNrZXJzID0gc3JjLnRpY2tlcnMoKQogICAgICAgIHJlc3VsdCA9IHt9CiAgICAgICAgZm9yIHQgaW4gdGlja2VyczoKICAgICAgICAgICAgc3ltID0gdC5nZXQoInN5bWJvbCIsICIiKQogICAgICAgICAgICBpZiBub3QgcmVxdWVzdGVkIG9yIHN5bSBpbiByZXF1ZXN0ZWQ6CiAgICAgICAgICAgICAgICByZXN1bHRbc3ltXSA9IHsKICAgICAgICAgICAgICAgICAgICAicHJpY2UiOiB0LmdldCgibGFzdF9wcmljZSIpLAogICAgICAgICAgICAgICAgICAgICJjaGFuZ2VfMjRoX3BjdCI6IHQuZ2V0KCJjaGFuZ2VfMjRoX3BjdCIpLAogICAgICAgICAgICAgICAgICAgICJoaWdoIjogdC5nZXQoImhpZ2giKSwKICAgICAgICAgICAgICAgICAgICAibG93IjogdC5nZXQoImxvdyIpLAogICAgICAgICAgICAgICAgICAgICJxdW90ZV92b2x1bWVfdXNkIjogdC5nZXQoInF1b3RlX3ZvbHVtZV91c2QiKSwKICAgICAgICAgICAgICAgIH0KICAgICAgICByZXR1cm4geyJ0cyI6IGRhdGV0aW1lLm5vdyh0aW1lem9uZS51dGMpLmlzb2Zvcm1hdCgpLCAicHJpY2VzIjogcmVzdWx0fQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHJhaXNlIEhUVFBFeGNlcHRpb24oNTAzLCBzdHIoZSkpCgoKQGFwcC5nZXQoIi9hcGkvY2FyZC97c3ltYm9sfSIpCmRlZiBjYXJkKHN5bWJvbDogc3RyLCBhY2NvdW50OiBmbG9hdCA9IDEwMDAwMCwgcmlza19wY3Q6IGZsb2F0ID0gMS4wLAogICAgICAgICB1c2VyX2xldmVyYWdlOiBmbG9hdCA9IFF1ZXJ5KGRlZmF1bHQ9Ni4wLCBnZT0xLjAsIGxlPTEwMC4wKSk6CiAgICB0cnk6CiAgICAgICAgc3JjID0gZ2V0X3NvdXJjZSgpCiAgICAgICAgc2Vjb25kYXJ5ID0gZ2V0X3NlY29uZGFyeSgpCiAgICAgICAgcmVzdWx0ID0gc2NvcmVfc3ltYm9sKHNyYywgc3ltYm9sLnVwcGVyKCksIGFjY291bnQsIHJpc2tfcGN0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWNvbmRhcnk9c2Vjb25kYXJ5LCB1c2VyX2xldmVyYWdlPXVzZXJfbGV2ZXJhZ2UpCiAgICAgICAgIyBMb2cgdGhlIHNpZ25hbCBmb3IgbGl2ZSBhY2N1cmFjeSB0cmFja2luZwogICAgICAgIHRyeToKICAgICAgICAgICAgbG9nX3NpZ25hbChyZXN1bHQpCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbjoKICAgICAgICAgICAgcGFzcwogICAgICAgIHJldHVybiByZXN1bHQKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByYWlzZSBIVFRQRXhjZXB0aW9uKDUwMywgc3RyKGUpKQoKCkBhcHAuZ2V0KCIvYXBpL3NpZ25hbHMiKQpkZWYgc2lnbmFscygpOgogICAgIiIiU2NhbiBtaWQtY2FwIHBlcnBzICh2b2x1bWUgcmFua3MgNi01MCkgZm9yIGV2ZW50LWRyaXZlbiBzaWduYWxzLgoKICAgIFVzZXMgNSBpbmRlcGVuZGVudCBkZXRlY3RvcnM6IFJTSSBleHRyZW1lIGNyb3NzLCB2b2x1bWUgc3VyZ2UsCiAgICBCQiBzcXVlZXplIHJlbGVhc2UsIGZ1bmRpbmcgcmF0ZSBleHRyZW1lLCBhbmQgMjRoIHByaWNlIHN1cmdlLgogICAgRWFjaCBmaXJlcyBpbmRlcGVuZGVudGx5IOKAlCBubyBjb21iaW5lZCBnYXRlIHJlcXVpcmVkLgogICAgQlRDIGFuZCBFVEggYWx3YXlzIGFwcGVuZGVkIGFzIG1hcmtldC13aWRlIHNlbnRpbWVudCBhbmNob3JzLgoKICAgIFJlc3VsdHMgY2FjaGVkIDUgbWluIHBlciBzeW1ib2wgaW5zaWRlIHNpZ25hbHMucHkuCiAgICAiIiIKICAgIHRyeToKICAgICAgICBzcmMgPSBnZXRfc291cmNlKCkKICAgICAgICBzZWNvbmRhcnkgPSBnZXRfc2Vjb25kYXJ5KCkKCiAgICAgICAgIyBCdWlsZCB1bml2ZXJzZTogbWlkLWNhcCByYW5rcyA2LTUwIGJ5IFVTRCB2b2x1bWUgKyBCVEMvRVRIIGFuY2hvcnMKICAgICAgICB0cnk6CiAgICAgICAgICAgIHN5bWJvbHMsIHRpY2tlcl9tYXAgPSBidWlsZF9zaWduYWxfdW5pdmVyc2Uoc3JjKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIHN5bWJvbHMgPSBnZXRhdHRyKHNyYywgImFuY2hvcnMiLCBbXSlbOjIwXQogICAgICAgICAgICB0aWNrZXJfbWFwID0ge30KCiAgICAgICAgYWxlcnRzID0gc2Nhbl91bml2ZXJzZShzcmMsIHN5bWJvbHMsIHNlY29uZGFyeT1zZWNvbmRhcnksIHRpY2tlcl9tYXA9dGlja2VyX21hcCkKICAgICAgICByZXR1cm4gewogICAgICAgICAgICAiZmV0Y2hlZF9hdCI6IGRhdGV0aW1lLm5vdyh0aW1lem9uZS51dGMpLmlzb2Zvcm1hdCgpLAogICAgICAgICAgICAic291cmNlIjogc3JjLm5hbWUsCiAgICAgICAgICAgICJzY2FubmVkIjogbGVuKHN5bWJvbHMpLAogICAgICAgICAgICAiYWxlcnRzIjogYWxlcnRzLAogICAgICAgIH0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByYWlzZSBIVFRQRXhjZXB0aW9uKDUwMywgc3RyKGUpKQoKCkBhcHAuZ2V0KCIvYXBpL25ld3MiKQpkZWYgbmV3cygpOgogICAgIiIiRnJlZSBjcnlwdG8gbmV3cyDigJQgQ29pbkdlY2tvIHRyZW5kaW5nICsgQ29pbkRlc2sgKyBDb2ludGVsZWdyYXBoIFJTUy4KCiAgICBObyBBUEkga2V5IHJlcXVpcmVkLiBSZXR1cm5zOgogICAgICB0cmVuZGluZ19jb2luczogdG9wIDcgY29pbnMgYnkgQ29pbkdlY2tvIHNlYXJjaCB2b2x1bWUgaW4gbGFzdCAyNGgKICAgICAgaGVhZGxpbmVzOiAgICAgIGxhdGVzdCBoZWFkbGluZXMgZnJvbSBDb2luRGVzayArIENvaW50ZWxlZ3JhcGggUlNTCiAgICAgICAgICAgICAgICAgICAgICBlYWNoIHRhZ2dlZCB3aXRoIGNvaW5zX21lbnRpb25lZFtdCiAgICBDYWNoZWQgMTAgbWluLgogICAgIiIiCiAgICB0cnk6CiAgICAgICAgcmVzdWx0ID0gZmV0Y2hfY3J5cHRvX2hlYWRsaW5lcygpCiAgICAgICAgcmV0dXJuIHJlc3VsdAogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHJhaXNlIEhUVFBFeGNlcHRpb24oNTAzLCBzdHIoZSkpCgoKQGFwcC5nZXQoIi9hcGkvc2lnbmFsLWFjY3VyYWN5IikKZGVmIHNpZ25hbF9hY2N1cmFjeShzeW1ib2w6IHN0ciA9ICIiKToKICAgICIiIkxpdmUgc2lnbmFsIGFjY3VyYWN5IOKAlCByZWFsIHdpbiByYXRlIGZyb20gbG9nZ2VkICsgcmVzb2x2ZWQgc2lnbmFscy4KCiAgICBUaGlzIGlzIGdyb3VuZCB0cnV0aDogZXZlcnkgY2FyZCB0aGUgc2Nhbm5lciBnZW5lcmF0ZXMgZ2V0cyBsb2dnZWQsCiAgICB0aGVuIGNoZWNrZWQgNDhoIGxhdGVyIHRvIHNlZSBpZiBUUDEgb3IgU0wgd2FzIGhpdCBmaXJzdC4KCiAgICA/c3ltYm9sPUJUQy1VU0RUICAob3B0aW9uYWwg4oCUIHJldHVybnMgcGVyLXN5bWJvbCBicmVha2Rvd24pCiAgICAiIiIKICAgIHRyeToKICAgICAgICBvdmVyYWxsID0gZ2V0X2FjY3VyYWN5KCkKICAgICAgICBpZiBzeW1ib2w6CiAgICAgICAgICAgIG92ZXJhbGxbInN5bWJvbF9icmVha2Rvd24iXSA9IGdldF9zeW1ib2xfYWNjdXJhY3koc3ltYm9sLnVwcGVyKCkpCiAgICAgICAgcmV0dXJuIG92ZXJhbGwKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByYWlzZSBIVFRQRXhjZXB0aW9uKDUwMywgc3RyKGUpKQoKCkBhcHAucG9zdCgiL2FwaS9yZXNvbHZlLXNpZ25hbHMiKQpkZWYgcmVzb2x2ZV9zaWduYWxzX2VuZHBvaW50KCk6CiAgICAiIiJUcmlnZ2VyIHJlc29sdXRpb24gb2YgYWxsIHBlbmRpbmcgc2lnbmFscyB0aGF0IGFyZSBwYXN0IHRoZWlyIDQ4aCB3aW5kb3cuCgogICAgQ2FsbGVkIG1hbnVhbGx5IG9yIGJ5IGEgc2NoZWR1bGVyLiBDaGVja3MgcHJpY2UgcmFuZ2Ugb24gZXhjaGFuZ2UKICAgIHRvIGRldGVybWluZSBpZiBUUDEgb3IgU0wgd2FzIHRvdWNoZWQgZHVyaW5nIHRoZSBob2xkIHBlcmlvZC4KICAgICIiIgogICAgdHJ5OgogICAgICAgIHNyYyA9IGdldF9zb3VyY2UoKQogICAgICAgIHJlc3VsdCA9IHJlc29sdmVfc2lnbmFscyhzcmMpCiAgICAgICAgcmV0dXJuIHJlc3VsdAogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHJhaXNlIEhUVFBFeGNlcHRpb24oNTAzLCBzdHIoZSkpCgoKQGFwcC5nZXQoIi9hcGkvY2F0YWx5c3QiKQpkZWYgY2F0YWx5c3Qoc3ltYm9sOiBzdHIgPSAiIik6CiAgICAiIiJEZWJ1ZyBlbmRwb2ludCDigJQgcmV0dXJucyByYXcgRmVhciAmIEdyZWVkICsgQ3J5cHRvUGFuaWMgZGF0YS4KICAgID9zeW1ib2w9QlRDLVVTRFQgIChvcHRpb25hbDsgcmV0dXJucyBjb2luIG5ld3MgaWYgcHJvdmlkZWQpCiAgICAiIiIKICAgIHRyeToKICAgICAgICBmbmcgPSBmZXRjaF9mZWFyX2dyZWVkKCkKICAgICAgICByZXN1bHQ6IGRpY3QgPSB7CiAgICAgICAgICAgICJmZWFyX2dyZWVkIjogewogICAgICAgICAgICAgICAgInZhbHVlIjogZm5nLmdldCgidmFsdWUiKSwKICAgICAgICAgICAgICAgICJsYWJlbCI6IGZuZy5nZXQoImxhYmVsIiksCiAgICAgICAgICAgICAgICAic2NvcmVfbW9kIjogZm5nLmdldCgic2NvcmVfbW9kIiksCiAgICAgICAgICAgICAgICAiZXJyb3IiOiBmbmcuZ2V0KCJlcnJvciIpLAogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIGlmIHN5bWJvbDoKICAgICAgICAgICAgbmV3cyA9IGZldGNoX2NvaW5fbmV3cyhzeW1ib2wudXBwZXIoKSkKICAgICAgICAgICAgY29tYmluZWRfc2NvcmUsIG5vdGVzLCBfcmF3ID0gc2NvcmVfY2F0YWx5c3Qoc3ltYm9sLnVwcGVyKCkpCiAgICAgICAgICAgIHJlc3VsdFsic3ltYm9sIl0gPSBzeW1ib2wudXBwZXIoKQogICAgICAgICAgICByZXN1bHRbIm5ld3MiXSA9IHsKICAgICAgICAgICAgICAgICJzY29yZSI6IG5ld3MuZ2V0KCJzY29yZSIpLAogICAgICAgICAgICAgICAgImxhYmVsIjogbmV3cy5nZXQoImxhYmVsIiksCiAgICAgICAgICAgICAgICAic291cmNlIjogbmV3cy5nZXQoInNvdXJjZSIpLAogICAgICAgICAgICAgICAgIml0ZW1zIjogbmV3cy5nZXQoIml0ZW1zIiwgW10pLAogICAgICAgICAgICAgICAgImVycm9yIjogbmV3cy5nZXQoImVycm9yIiksCiAgICAgICAgICAgIH0KICAgICAgICAgICAgcmVzdWx0WyJjYXRhbHlzdF9zY29yZSJdID0gY29tYmluZWRfc2NvcmUKICAgICAgICAgICAgcmVzdWx0WyJub3RlcyJdID0gbm90ZXMKICAgICAgICByZXN1bHRbImNyeXB0b3BhbmljX2tleV9zZXQiXSA9IGJvb2woQ1JZUFRPUEFOSUNfVE9LRU4pCiAgICAgICAgcmVzdWx0WyJ0cyJdID0gZGF0ZXRpbWUubm93KHRpbWV6b25lLnV0YykuaXNvZm9ybWF0KCkKICAgICAgICByZXR1cm4gcmVzdWx0CiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmFpc2UgSFRUUEV4Y2VwdGlvbig1MDMsIHN0cihlKSkKCgpAYXBwLmdldCgiL2FwaS90ZXN0LXRlbGVncmFtIikKZGVmIHRlc3RfdGVsZWdyYW0oKToKICAgICIiIkRpYWdub3NlIFRlbGVncmFtIHJlbGF5OiBwcm9iZSBXb3JrZXIgcmVhY2hhYmlsaXR5IHRoZW4gc2VuZCBtZXNzYWdlLiIiIgogICAgaW1wb3J0IHJlcXVlc3RzIGFzIHJlcQogICAgd29ya2VyX3VybCA9IG9zLmVudmlyb24uZ2V0KCJDTE9VREZMQVJFX1dPUktFUl9VUkwiLCAiIikKICAgIGlmIG5vdCB3b3JrZXJfdXJsOgogICAgICAgIHJldHVybiBKU09OUmVzcG9uc2UoeyJvayI6IEZhbHNlLCAiZXJyb3IiOiAiQ0xPVURGTEFSRV9XT1JLRVJfVVJMIG5vdCBzZXQgaW4gSEYgU2VjcmV0cyJ9LCBzdGF0dXNfY29kZT01MDApCgogICAgIyBQcm9iZTogR0VUIHRoZSB3b3JrZXIgKGV4cGVjdCA0MDUgLSBwcm92ZXMgSEYgY2FuIHJlYWNoIHdvcmtlcnMuZGV2KQogICAgcHJvYmUgPSB7fQogICAgdHJ5OgogICAgICAgIHIgPSByZXEuZ2V0KHdvcmtlcl91cmwsIHRpbWVvdXQ9OCkKICAgICAgICBwcm9iZVsid29ya2VyX2dldCJdID0geyJzdGF0dXMiOiByLnN0YXR1c19jb2RlLCAiYm9keSI6IHIudGV4dFs6ODBdfQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgIHByb2JlWyJ3b3JrZXJfZ2V0Il0gPSB7ImVycm9yIjogdHlwZShlKS5fX25hbWVfXyArICI6ICIgKyBzdHIoZSlbOjEyMF19CgogICAgIyBTZW5kIHRoZSBUZWxlZ3JhbSBtZXNzYWdlIHdpdGggbG9uZ2VyIHRpbWVvdXQKICAgIG1zZyA9ICJUZXN0OiBUcmFkZSBDb3BpbG90IHJlbGF5IGNoZWNrIHZpYSBDbG91ZGZsYXJlIFdvcmtlci4iCiAgICB0cnk6CiAgICAgICAgcmVzcCA9IHJlcS5wb3N0KHdvcmtlcl91cmwsIGpzb249eyJ0ZXh0IjogbXNnLCAicGFyc2VfbW9kZSI6ICJIVE1MIn0sIHRpbWVvdXQ9MzApCiAgICAgICAgYm9keSA9IHJlc3AuanNvbigpCiAgICAgICAgaWYgcmVzcC5zdGF0dXNfY29kZSA9PSAyMDA6CiAgICAgICAgICAgIHJldHVybiB7Im9rIjogVHJ1ZSwgIm1lc3NhZ2UiOiAiVGVzdCBtZXNzYWdlIHNlbnQgdmlhIENGIFdvcmtlciIsICJwcm9iZSI6IHByb2JlfQogICAgICAgIHJldHVybiBKU09OUmVzcG9uc2UoeyJvayI6IEZhbHNlLCAic3RhdHVzIjogcmVzcC5zdGF0dXNfY29kZSwgImVycm9yIjogYm9keSwgInByb2JlIjogcHJvYmV9LCBzdGF0dXNfY29kZT01MDApCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmV0dXJuIEpTT05SZXNwb25zZSh7Im9rIjogRmFsc2UsICJlcnJvciI6IHR5cGUoZSkuX19uYW1lX18gKyAiOiAiICsgc3RyKGUpWzoyMDBdLCAicHJvYmUiOiBwcm9iZX0sIHN0YXR1c19jb2RlPTUwMCkKCgpAYXBwLmdldCgiL2FwaS9iYWNrdGVzdC97c3ltYm9sfSIpCmRlZiBiYWNrdGVzdChzeW1ib2w6IHN0cik6CiAgICAiIiJXYWxrLWZvcndhcmQgYmFja3Rlc3QgZm9yIG9uZSBzeW1ib2wgb3ZlciB+NiBtb250aHMgb2YgMWggY2FuZGxlcy4KCiAgICBXaW4gY29uZGl0aW9uOiBUUDIgaGl0IGJlZm9yZSBTTC4KICAgIFJlc3VsdHMgY2FjaGVkIDZoIOKAlCBmaXJzdCBjYWxsIGlzIHNsb3cgKH41LTE1cyksIHN1YnNlcXVlbnQgY2FsbHMgaW5zdGFudC4KCiAgICBSZXR1cm5zOgogICAgICB0b3RhbF90cmFkZXMsIHdpbl9yYXRlLCBhdmdfciwgbWF4X2RyYXdkb3duX3IsCiAgICAgIGJ5X3N0YXRlIHtCVUxML0JFQVIvUkFOR0lORzoge3RyYWRlcywgd2lucywgd2luX3JhdGUsIGF2Z19yfX0sCiAgICAgIGJ5X2RpcmVjdGlvbiB7bG9uZy9zaG9ydDogLi4ufSwKICAgICAgZXF1aXR5X2N1cnZlIFtmbG9hdCBsaXN0XSwKICAgICAgcmVjZW50X3RyYWRlcyBbbGFzdCA1MF0KICAgICIiIgogICAgdHJ5OgogICAgICAgIHNyYyA9IGdldF9zb3VyY2UoKQogICAgICAgIHJlc3VsdCA9IGJhY2t0ZXN0X3N5bWJvbChzcmMsIHN5bWJvbC51cHBlcigpKQogICAgICAgIHJldHVybiByZXN1bHQKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByYWlzZSBIVFRQRXhjZXB0aW9uKDUwMywgc3RyKGUpKQoKCiMg4pSA4pSAIFN0YXRpYyBkYXNoYm9hcmQg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpzdGF0aWNfZGlyID0gUGF0aChfX2ZpbGVfXykucGFyZW50IC8gInN0YXRpYyIKaWYgc3RhdGljX2Rpci5leGlzdHMoKToKICAgIGFwcC5tb3VudCgiL3N0YXRpYyIsIFN0YXRpY0ZpbGVzKGRpcmVjdG9yeT1zdHIoc3RhdGljX2RpcikpLCBuYW1lPSJzdGF0aWMiKQoKQGFwcC5nZXQoIi8iKQpkZWYgcm9vdCgpOgogICAgaW5kZXggPSBzdGF0aWNfZGlyIC8gImluZGV4Lmh0bWwiCiAgICBpZiBpbmRleC5leGlzdHMoKToKICAgICAgICByZXR1cm4gRmlsZVJlc3BvbnNlKHN0cihpbmRleCkpCiAgICByZXR1cm4geyJtZXNzYWdlIjogIlRyYWRlIENvcGlsb3QgQVBJIHJ1bm5pbmcuIFVzZSAvYXBpL3NjYW4gb3IgL2FwaS9zdGF0dXMuIn0KCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgaW1wb3J0IHV2aWNvcm4KICAgIHBvcnQgPSBpbnQob3MuZW52aXJvbi5nZXQoIlBPUlQiLCA3ODYwKSkKICAgIHV2aWNvcm4ucnVuKCJtYWluOmFwcCIsIGhvc3Q9IjAuMC4wLjAiLCBwb3J0PXBvcnQpCg==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trade Copilot — FastAPI server.
|
| 2 |
+
|
| 3 |
+
Endpoints:
|
| 4 |
+
GET / → dashboard HTML (served from static/index.html)
|
| 5 |
+
GET /api/scan → ranked shortlist JSON
|
| 6 |
+
GET /api/card/{symbol} → full trade card for one symbol
|
| 7 |
+
GET /api/status → health + active source
|
| 8 |
+
|
| 9 |
+
All computation is deterministic — no AI in the data path.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
import math, statistics, time, os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from functools import lru_cache
|
| 15 |
+
from datetime import datetime, timezone
|
| 16 |
+
|
| 17 |
+
import asyncio
|
| 18 |
+
import logging
|
| 19 |
+
from contextlib import asynccontextmanager
|
| 20 |
+
|
| 21 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 22 |
+
from fastapi.staticfiles import StaticFiles
|
| 23 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 24 |
+
|
| 25 |
+
from exchange import pick_source, SOURCES, BingX, Binance
|
| 26 |
+
from scorer import score_symbol
|
| 27 |
+
from catalyst import fetch_fear_greed, fetch_coin_news, score_catalyst, fetch_crypto_headlines
|
| 28 |
+
from signals import scan_universe, build_signal_universe
|
| 29 |
+
from backtest import backtest_symbol
|
| 30 |
+
from signal_log import log_signal, resolve_signals, get_accuracy, get_symbol_accuracy
|
| 31 |
+
|
| 32 |
+
# CryptoPanic token — set in HF Spaces Secrets as CRYPTOPANIC_TOKEN
|
| 33 |
+
# Never hardcode or log this value
|
| 34 |
+
CRYPTOPANIC_TOKEN = os.environ.get("CRYPTOPANIC_TOKEN", "")
|
| 35 |
+
|
| 36 |
+
logger = logging.getLogger("main")
|
| 37 |
+
|
| 38 |
+
ALERT_INTERVAL = 300 # 5 minutes
|
| 39 |
+
|
| 40 |
+
async def _alert_loop():
|
| 41 |
+
"""Background task: scan every 5 min and fire Telegram alerts for high-confidence cards."""
|
| 42 |
+
from telegram_alert import send_alert
|
| 43 |
+
# Read account size from HF Secret — defaults to 10 if not set
|
| 44 |
+
try:
|
| 45 |
+
alert_account = float(os.environ.get("ALERT_ACCOUNT", "10"))
|
| 46 |
+
except ValueError:
|
| 47 |
+
alert_account = 10.0
|
| 48 |
+
logger.info("Alert loop starting — account=$%.2f", alert_account)
|
| 49 |
+
|
| 50 |
+
await asyncio.sleep(60) # give app time to fully boot before first scan
|
| 51 |
+
while True:
|
| 52 |
+
try:
|
| 53 |
+
src = get_source()
|
| 54 |
+
secondary = get_secondary()
|
| 55 |
+
candidates = rank_universe(src, src.min_vol, 30)
|
| 56 |
+
for c in candidates:
|
| 57 |
+
try:
|
| 58 |
+
card = score_symbol(src, c["symbol"], alert_account, 1.0,
|
| 59 |
+
secondary=secondary, user_leverage=6.0)
|
| 60 |
+
conf = card.get("confidence", 0)
|
| 61 |
+
vetoed = card.get("vetoed", False)
|
| 62 |
+
if conf >= 8.8 and not vetoed:
|
| 63 |
+
tv_sym = src.tv_symbol(card["symbol"])
|
| 64 |
+
await send_alert(card, tv_sym)
|
| 65 |
+
await asyncio.sleep(1) # small gap between sends
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logger.debug("Alert scan error for %s: %s", c.get("symbol"), e)
|
| 68 |
+
await asyncio.sleep(0.5) # gentle rate limit between symbols
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logger.error("Alert loop error: %s", e)
|
| 71 |
+
await asyncio.sleep(ALERT_INTERVAL)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# Event loop reference — captured at startup so sync endpoints can schedule coroutines
|
| 75 |
+
_event_loop: asyncio.AbstractEventLoop | None = None
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@asynccontextmanager
|
| 79 |
+
async def lifespan(app):
|
| 80 |
+
global _event_loop
|
| 81 |
+
_event_loop = asyncio.get_running_loop()
|
| 82 |
+
task = asyncio.create_task(_alert_loop())
|
| 83 |
+
try:
|
| 84 |
+
yield
|
| 85 |
+
finally:
|
| 86 |
+
task.cancel()
|
| 87 |
+
try:
|
| 88 |
+
await task
|
| 89 |
+
except asyncio.CancelledError:
|
| 90 |
+
pass
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
app = FastAPI(title="Trade Copilot", version="1.0", lifespan=lifespan)
|
| 94 |
+
|
| 95 |
+
# Cache the active source for 5 min (avoids re-pinging on every request)
|
| 96 |
+
_source_cache: dict = {}
|
| 97 |
+
_source_ts: float = 0
|
| 98 |
+
SOURCE_TTL = 300
|
| 99 |
+
|
| 100 |
+
# Secondary source for cross-validation (Binance — deeper OI/funding data)
|
| 101 |
+
_binance: Binance | None = None
|
| 102 |
+
_binance_ok: bool = False
|
| 103 |
+
_binance_ts: float = 0
|
| 104 |
+
BINANCE_TTL = 300
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def get_source():
|
| 108 |
+
global _source_cache, _source_ts
|
| 109 |
+
if time.time() - _source_ts > SOURCE_TTL or not _source_cache.get("src"):
|
| 110 |
+
src = pick_source()
|
| 111 |
+
_source_cache = {"src": src}
|
| 112 |
+
_source_ts = time.time()
|
| 113 |
+
return _source_cache["src"]
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def get_secondary():
|
| 117 |
+
"""Returns Binance instance if reachable, else None. Used for cross-validation."""
|
| 118 |
+
global _binance, _binance_ok, _binance_ts
|
| 119 |
+
if time.time() - _binance_ts < BINANCE_TTL:
|
| 120 |
+
return _binance if _binance_ok else None
|
| 121 |
+
try:
|
| 122 |
+
b = Binance()
|
| 123 |
+
b.ping()
|
| 124 |
+
_binance = b
|
| 125 |
+
_binance_ok = True
|
| 126 |
+
except Exception:
|
| 127 |
+
_binance = None
|
| 128 |
+
_binance_ok = False
|
| 129 |
+
_binance_ts = time.time()
|
| 130 |
+
return _binance if _binance_ok else None
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def rank_universe(src, min_volume: float, top_n: int) -> list[dict]:
|
| 134 |
+
perps = src.universe()
|
| 135 |
+
rows = []
|
| 136 |
+
for t in src.tickers():
|
| 137 |
+
if t["symbol"] not in perps:
|
| 138 |
+
continue
|
| 139 |
+
qv = t["quote_volume_usd"]
|
| 140 |
+
lo, hi = t.get("low", 0), t.get("high", 0)
|
| 141 |
+
if qv < min_volume or lo <= 0:
|
| 142 |
+
continue
|
| 143 |
+
t["range_24h_pct"] = (hi - lo) / lo * 100
|
| 144 |
+
rows.append(t)
|
| 145 |
+
if len(rows) < 3:
|
| 146 |
+
return [{"symbol": s} for s in src.anchors]
|
| 147 |
+
|
| 148 |
+
log_vols = [math.log(r["quote_volume_usd"]) for r in rows]
|
| 149 |
+
ranges = [r["range_24h_pct"] for r in rows]
|
| 150 |
+
mv, sv = statistics.mean(log_vols), statistics.stdev(log_vols)
|
| 151 |
+
mr, sr = statistics.mean(ranges), statistics.stdev(ranges)
|
| 152 |
+
for r, lv, rg in zip(rows, log_vols, ranges):
|
| 153 |
+
r["scan_score"] = round(0.5 * (lv - mv) / sv + 0.5 * (rg - mr) / sr, 3)
|
| 154 |
+
|
| 155 |
+
rows.sort(key=lambda r: r["scan_score"], reverse=True)
|
| 156 |
+
picked = rows[:top_n]
|
| 157 |
+
picked_syms = {r["symbol"] for r in picked}
|
| 158 |
+
for sym in src.anchors:
|
| 159 |
+
if sym not in picked_syms:
|
| 160 |
+
extra = next((r for r in rows if r["symbol"] == sym), None)
|
| 161 |
+
if extra:
|
| 162 |
+
extra["anchor"] = True
|
| 163 |
+
picked.append(extra)
|
| 164 |
+
return picked
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# ── API routes ─────────────────────────────────────────────────────────────
|
| 168 |
+
|
| 169 |
+
@app.get("/api/status")
|
| 170 |
+
def status():
|
| 171 |
+
try:
|
| 172 |
+
src = get_source()
|
| 173 |
+
return {"ok": True, "source": src.name, "market": src.market_note,
|
| 174 |
+
"ts": datetime.now(timezone.utc).isoformat()}
|
| 175 |
+
except Exception as e:
|
| 176 |
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=503)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@app.get("/api/scan")
|
| 180 |
+
def scan(top: int = 8, min_volume: float = None, account: float = 100000,
|
| 181 |
+
risk_pct: float = 1.0,
|
| 182 |
+
user_leverage: float = Query(default=6.0, ge=1.0, le=100.0)):
|
| 183 |
+
try:
|
| 184 |
+
src = get_source()
|
| 185 |
+
floor = min_volume or src.min_vol
|
| 186 |
+
candidates = rank_universe(src, floor, top)
|
| 187 |
+
secondary = get_secondary()
|
| 188 |
+
cards = []
|
| 189 |
+
for c in candidates:
|
| 190 |
+
try:
|
| 191 |
+
card = score_symbol(src, c["symbol"], account, risk_pct,
|
| 192 |
+
secondary=secondary, user_leverage=user_leverage)
|
| 193 |
+
card["rank_info"] = {k: c.get(k) for k in
|
| 194 |
+
("scan_score","change_24h_pct","range_24h_pct",
|
| 195 |
+
"quote_volume_usd","anchor")}
|
| 196 |
+
cards.append(card)
|
| 197 |
+
# Auto-log every non-vetoed signal for live accuracy tracking
|
| 198 |
+
try:
|
| 199 |
+
log_signal(card)
|
| 200 |
+
except Exception:
|
| 201 |
+
pass
|
| 202 |
+
# Fire Telegram alert if confidence >= 9.0
|
| 203 |
+
try:
|
| 204 |
+
conf = card.get("confidence", 0)
|
| 205 |
+
vetoed = card.get("vetoed", False)
|
| 206 |
+
if conf >= 8.8 and not vetoed and _event_loop is not None:
|
| 207 |
+
from telegram_alert import send_alert
|
| 208 |
+
tv_sym = src.tv_symbol(card["symbol"])
|
| 209 |
+
asyncio.run_coroutine_threadsafe(
|
| 210 |
+
send_alert(card, tv_sym, manual=True), _event_loop
|
| 211 |
+
)
|
| 212 |
+
except Exception:
|
| 213 |
+
pass
|
| 214 |
+
time.sleep(0.05)
|
| 215 |
+
except Exception as e:
|
| 216 |
+
cards.append({"symbol": c["symbol"], "error": str(e)[:120]})
|
| 217 |
+
cards.sort(key=lambda c: c.get("confidence", 0), reverse=True)
|
| 218 |
+
return {"fetched_at": datetime.now(timezone.utc).isoformat(),
|
| 219 |
+
"source": src.name, "market": src.market_note, "cards": cards}
|
| 220 |
+
except Exception as e:
|
| 221 |
+
raise HTTPException(503, str(e))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
@app.get("/api/prices")
|
| 225 |
+
def prices(symbols: str = ""):
|
| 226 |
+
"""Lightweight price ticker — returns current price + 24h change for a
|
| 227 |
+
comma-separated list of symbols. Used for real-time card price updates."""
|
| 228 |
+
try:
|
| 229 |
+
src = get_source()
|
| 230 |
+
requested = {s.strip() for s in symbols.split(",") if s.strip()}
|
| 231 |
+
tickers = src.tickers()
|
| 232 |
+
result = {}
|
| 233 |
+
for t in tickers:
|
| 234 |
+
sym = t.get("symbol", "")
|
| 235 |
+
if not requested or sym in requested:
|
| 236 |
+
result[sym] = {
|
| 237 |
+
"price": t.get("last_price"),
|
| 238 |
+
"change_24h_pct": t.get("change_24h_pct"),
|
| 239 |
+
"high": t.get("high"),
|
| 240 |
+
"low": t.get("low"),
|
| 241 |
+
"quote_volume_usd": t.get("quote_volume_usd"),
|
| 242 |
+
}
|
| 243 |
+
return {"ts": datetime.now(timezone.utc).isoformat(), "prices": result}
|
| 244 |
+
except Exception as e:
|
| 245 |
+
raise HTTPException(503, str(e))
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@app.get("/api/card/{symbol}")
|
| 249 |
+
def card(symbol: str, account: float = 100000, risk_pct: float = 1.0,
|
| 250 |
+
user_leverage: float = Query(default=6.0, ge=1.0, le=100.0)):
|
| 251 |
+
try:
|
| 252 |
+
src = get_source()
|
| 253 |
+
secondary = get_secondary()
|
| 254 |
+
result = score_symbol(src, symbol.upper(), account, risk_pct,
|
| 255 |
+
secondary=secondary, user_leverage=user_leverage)
|
| 256 |
+
# Log the signal for live accuracy tracking
|
| 257 |
+
try:
|
| 258 |
+
log_signal(result)
|
| 259 |
+
except Exception:
|
| 260 |
+
pass
|
| 261 |
+
return result
|
| 262 |
+
except Exception as e:
|
| 263 |
+
raise HTTPException(503, str(e))
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
@app.get("/api/signals")
|
| 267 |
+
def signals():
|
| 268 |
+
"""Scan mid-cap perps (volume ranks 6-50) for event-driven signals.
|
| 269 |
+
|
| 270 |
+
Uses 5 independent detectors: RSI extreme cross, volume surge,
|
| 271 |
+
BB squeeze release, funding rate extreme, and 24h price surge.
|
| 272 |
+
Each fires independently — no combined gate required.
|
| 273 |
+
BTC and ETH always appended as market-wide sentiment anchors.
|
| 274 |
+
|
| 275 |
+
Results cached 5 min per symbol inside signals.py.
|
| 276 |
+
"""
|
| 277 |
+
try:
|
| 278 |
+
src = get_source()
|
| 279 |
+
secondary = get_secondary()
|
| 280 |
+
|
| 281 |
+
# Build universe: mid-cap ranks 6-50 by USD volume + BTC/ETH anchors
|
| 282 |
+
try:
|
| 283 |
+
symbols, ticker_map = build_signal_universe(src)
|
| 284 |
+
except Exception:
|
| 285 |
+
symbols = getattr(src, "anchors", [])[:20]
|
| 286 |
+
ticker_map = {}
|
| 287 |
+
|
| 288 |
+
alerts = scan_universe(src, symbols, secondary=secondary, ticker_map=ticker_map)
|
| 289 |
+
return {
|
| 290 |
+
"fetched_at": datetime.now(timezone.utc).isoformat(),
|
| 291 |
+
"source": src.name,
|
| 292 |
+
"scanned": len(symbols),
|
| 293 |
+
"alerts": alerts,
|
| 294 |
+
}
|
| 295 |
+
except Exception as e:
|
| 296 |
+
raise HTTPException(503, str(e))
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
@app.get("/api/news")
|
| 300 |
+
def news():
|
| 301 |
+
"""Free crypto news — CoinGecko trending + CoinDesk + Cointelegraph RSS.
|
| 302 |
+
|
| 303 |
+
No API key required. Returns:
|
| 304 |
+
trending_coins: top 7 coins by CoinGecko search volume in last 24h
|
| 305 |
+
headlines: latest headlines from CoinDesk + Cointelegraph RSS
|
| 306 |
+
each tagged with coins_mentioned[]
|
| 307 |
+
Cached 10 min.
|
| 308 |
+
"""
|
| 309 |
+
try:
|
| 310 |
+
result = fetch_crypto_headlines()
|
| 311 |
+
return result
|
| 312 |
+
except Exception as e:
|
| 313 |
+
raise HTTPException(503, str(e))
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
@app.get("/api/signal-accuracy")
|
| 317 |
+
def signal_accuracy(symbol: str = ""):
|
| 318 |
+
"""Live signal accuracy — real win rate from logged + resolved signals.
|
| 319 |
+
|
| 320 |
+
This is ground truth: every card the scanner generates gets logged,
|
| 321 |
+
then checked 48h later to see if TP1 or SL was hit first.
|
| 322 |
+
|
| 323 |
+
?symbol=BTC-USDT (optional — returns per-symbol breakdown)
|
| 324 |
+
"""
|
| 325 |
+
try:
|
| 326 |
+
overall = get_accuracy()
|
| 327 |
+
if symbol:
|
| 328 |
+
overall["symbol_breakdown"] = get_symbol_accuracy(symbol.upper())
|
| 329 |
+
return overall
|
| 330 |
+
except Exception as e:
|
| 331 |
+
raise HTTPException(503, str(e))
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
@app.post("/api/resolve-signals")
|
| 335 |
+
def resolve_signals_endpoint():
|
| 336 |
+
"""Trigger resolution of all pending signals that are past their 48h window.
|
| 337 |
+
|
| 338 |
+
Called manually or by a scheduler. Checks price range on exchange
|
| 339 |
+
to determine if TP1 or SL was touched during the hold period.
|
| 340 |
+
"""
|
| 341 |
+
try:
|
| 342 |
+
src = get_source()
|
| 343 |
+
result = resolve_signals(src)
|
| 344 |
+
return result
|
| 345 |
+
except Exception as e:
|
| 346 |
+
raise HTTPException(503, str(e))
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
@app.get("/api/catalyst")
|
| 350 |
+
def catalyst(symbol: str = ""):
|
| 351 |
+
"""Debug endpoint — returns raw Fear & Greed + CryptoPanic data.
|
| 352 |
+
?symbol=BTC-USDT (optional; returns coin news if provided)
|
| 353 |
+
"""
|
| 354 |
+
try:
|
| 355 |
+
fng = fetch_fear_greed()
|
| 356 |
+
result: dict = {
|
| 357 |
+
"fear_greed": {
|
| 358 |
+
"value": fng.get("value"),
|
| 359 |
+
"label": fng.get("label"),
|
| 360 |
+
"score_mod": fng.get("score_mod"),
|
| 361 |
+
"error": fng.get("error"),
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
if symbol:
|
| 365 |
+
news = fetch_coin_news(symbol.upper())
|
| 366 |
+
combined_score, notes, _raw = score_catalyst(symbol.upper())
|
| 367 |
+
result["symbol"] = symbol.upper()
|
| 368 |
+
result["news"] = {
|
| 369 |
+
"score": news.get("score"),
|
| 370 |
+
"label": news.get("label"),
|
| 371 |
+
"source": news.get("source"),
|
| 372 |
+
"items": news.get("items", []),
|
| 373 |
+
"error": news.get("error"),
|
| 374 |
+
}
|
| 375 |
+
result["catalyst_score"] = combined_score
|
| 376 |
+
result["notes"] = notes
|
| 377 |
+
result["cryptopanic_key_set"] = bool(CRYPTOPANIC_TOKEN)
|
| 378 |
+
result["ts"] = datetime.now(timezone.utc).isoformat()
|
| 379 |
+
return result
|
| 380 |
+
except Exception as e:
|
| 381 |
+
raise HTTPException(503, str(e))
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
@app.get("/api/test-telegram")
|
| 385 |
+
def test_telegram():
|
| 386 |
+
"""Diagnose Telegram relay: probe Worker reachability then send message."""
|
| 387 |
+
import requests as req
|
| 388 |
+
worker_url = os.environ.get("CLOUDFLARE_WORKER_URL", "")
|
| 389 |
+
if not worker_url:
|
| 390 |
+
return JSONResponse({"ok": False, "error": "CLOUDFLARE_WORKER_URL not set in HF Secrets"}, status_code=500)
|
| 391 |
+
|
| 392 |
+
# Probe: GET the worker (expect 405 - proves HF can reach workers.dev)
|
| 393 |
+
probe = {}
|
| 394 |
+
try:
|
| 395 |
+
r = req.get(worker_url, timeout=8)
|
| 396 |
+
probe["worker_get"] = {"status": r.status_code, "body": r.text[:80]}
|
| 397 |
+
except Exception as e:
|
| 398 |
+
probe["worker_get"] = {"error": type(e).__name__ + ": " + str(e)[:120]}
|
| 399 |
+
|
| 400 |
+
# Send the Telegram message with longer timeout
|
| 401 |
+
msg = "Test: Trade Copilot relay check via Cloudflare Worker."
|
| 402 |
+
try:
|
| 403 |
+
resp = req.post(worker_url, json={"text": msg, "parse_mode": "HTML"}, timeout=30)
|
| 404 |
+
body = resp.json()
|
| 405 |
+
if resp.status_code == 200:
|
| 406 |
+
return {"ok": True, "message": "Test message sent via CF Worker", "probe": probe}
|
| 407 |
+
return JSONResponse({"ok": False, "status": resp.status_code, "error": body, "probe": probe}, status_code=500)
|
| 408 |
+
except Exception as e:
|
| 409 |
+
return JSONResponse({"ok": False, "error": type(e).__name__ + ": " + str(e)[:200], "probe": probe}, status_code=500)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
@app.get("/api/backtest/{symbol}")
|
| 413 |
+
def backtest(symbol: str):
|
| 414 |
+
"""Walk-forward backtest for one symbol over ~6 months of 1h candles.
|
| 415 |
+
|
| 416 |
+
Win condition: TP2 hit before SL.
|
| 417 |
+
Results cached 6h — first call is slow (~5-15s), subsequent calls instant.
|
| 418 |
+
|
| 419 |
+
Returns:
|
| 420 |
+
total_trades, win_rate, avg_r, max_drawdown_r,
|
| 421 |
+
by_state {BULL/BEAR/RANGING: {trades, wins, win_rate, avg_r}},
|
| 422 |
+
by_direction {long/short: ...},
|
| 423 |
+
equity_curve [float list],
|
| 424 |
+
recent_trades [last 50]
|
| 425 |
+
"""
|
| 426 |
+
try:
|
| 427 |
+
src = get_source()
|
| 428 |
+
result = backtest_symbol(src, symbol.upper())
|
| 429 |
+
return result
|
| 430 |
+
except Exception as e:
|
| 431 |
+
raise HTTPException(503, str(e))
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
# ── Static dashboard ───────────────────────────────────────────────────────
|
| 435 |
+
|
| 436 |
+
static_dir = Path(__file__).parent / "static"
|
| 437 |
+
if static_dir.exists():
|
| 438 |
+
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
| 439 |
+
|
| 440 |
+
@app.get("/")
|
| 441 |
+
def root():
|
| 442 |
+
index = static_dir / "index.html"
|
| 443 |
+
if index.exists():
|
| 444 |
+
return FileResponse(str(index))
|
| 445 |
+
return {"message": "Trade Copilot API running. Use /api/scan or /api/status."}
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
if __name__ == "__main__":
|
| 449 |
+
import uvicorn
|
| 450 |
+
port = int(os.environ.get("PORT", 7860))
|
| 451 |
+
uvicorn.run("main:app", host="0.0.0.0", port=port)
|