diff --git a/.gitignore b/.gitignore index b1a3fcf..77a029b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ sample/ sample/* +.exports +index.html \ No newline at end of file diff --git a/Pipfile b/Pipfile index 2fc1c68..f48a63f 100644 --- a/Pipfile +++ b/Pipfile @@ -17,8 +17,13 @@ flask-migrate = "*" flask-sqlalchemy = "*" gunicorn = "*" "psycopg2-binary" = "*" - +"requests_oauthlib" = "*" +"oauthlib" = "*" +"python_graphql_client" = "*" +"plotly" = "*" +"pandas" = "*" +"numpy" = "*" [requires] -python_version = "3.6" +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index c70a46a..b72fcde 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "edf1bcf93c642f3e5d1b27e0a88b3615a4e5b50c57e8d9cec52b1b31943d8399" + "sha256": "464281d16c70818722d76ff9a1404ae496657a4a299f534c86725dab898f6f6f" }, "pipfile-spec": 6, "requires": { - "python_version": "3.6" + "python_version": "3.9" }, "sources": [ { @@ -16,18 +16,79 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", + "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", + "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", + "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", + "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", + "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", + "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", + "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", + "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", + "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", + "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", + "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", + "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", + "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", + "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", + "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", + "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", + "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", + "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", + "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", + "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", + "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", + "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", + "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", + "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", + "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", + "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", + "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", + "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", + "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", + "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", + "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", + "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", + "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", + "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", + "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", + "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" + ], + "markers": "python_version >= '3.6'", + "version": "==3.7.3" + }, "alembic": { "hashes": [ - "sha256:cdb7d98bd5cbf65acd38d70b1c05573c432e6473a82f955cdea541b5c153b0cc" + "sha256:a4de8d3525a95a96d59342e14b95cab5956c25b0907dce1549bb4e3e7958f4c2", + "sha256:c057488cc8ac7c4d06025ea3907e1a4dd07af70376fa149cf6bd2bc11b43076f" ], - "version": "==1.0.11" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.5.2" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2019.6.16" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -38,69 +99,74 @@ }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "version": "==7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" }, "flask": { "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", + "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.1.2" }, "flask-migrate": { "hashes": [ - "sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1", - "sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502" + "sha256:8626af845e6071ef80c70b0dc16d373f761c981f0ad61bb143a529cab649e725", + "sha256:c1601dfd46b9204233935e5d73473cd7fa959db7a4b0e894c7aa7a9e8aeebf0e" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.6.0" }, "flask-sqlalchemy": { "hashes": [ - "sha256:0c9609b0d72871c540a7945ea559c8fdf5455192d2db67219509aed680a3d45a", - "sha256:8631bbea987bc3eb0f72b1f691d47bd37ceb795e73b59ab48586d76d75a7c605" + "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", + "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" ], "index": "pypi", - "version": "==2.4.0" + "version": "==2.4.4" }, "gunicorn": { "hashes": [ - "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", - "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" ], "index": "pypi", - "version": "==19.9.0" + "version": "==20.0.4" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "jinja2": { "hashes": [ - "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", - "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", + "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], - "version": "==2.10.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.11.2" }, "mako": { "hashes": [ - "sha256:f5a642d8c5699269ab62a68b296ff990767eb120f51e2e8f3d6afb16bdb57f4b" + "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab" ], - "version": "==1.0.14" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.4" }, "markupsafe": { "hashes": [ @@ -108,13 +174,16 @@ "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", @@ -131,93 +200,378 @@ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, + "multidict": { + "hashes": [ + "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", + "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", + "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", + "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", + "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", + "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", + "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", + "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", + "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", + "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", + "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", + "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", + "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", + "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", + "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", + "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", + "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", + "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", + "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", + "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", + "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", + "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", + "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", + "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", + "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", + "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", + "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", + "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", + "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", + "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", + "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", + "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", + "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", + "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", + "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", + "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", + "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "numpy": { + "hashes": [ + "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94", + "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080", + "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e", + "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c", + "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76", + "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371", + "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c", + "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2", + "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a", + "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb", + "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140", + "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28", + "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f", + "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d", + "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff", + "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8", + "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa", + "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea", + "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc", + "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73", + "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d", + "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d", + "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4", + "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c", + "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e", + "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea", + "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd", + "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f", + "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff", + "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e", + "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7", + "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa", + "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827", + "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60" + ], + "index": "pypi", + "version": "==1.19.5" + }, + "oauthlib": { + "hashes": [ + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + ], + "index": "pypi", + "version": "==3.1.0" + }, + "pandas": { + "hashes": [ + "sha256:050ed2c9d825ef36738e018454e6d055c63d947c1d52010fbadd7584f09df5db", + "sha256:055647e7f4c5e66ba92c2a7dcae6c2c57898b605a3fb007745df61cc4015937f", + "sha256:23ac77a3a222d9304cb2a7934bb7b4805ff43d513add7a42d1a22dc7df14edd2", + "sha256:2de012a36cc507debd9c3351b4d757f828d5a784a5fc4e6766eafc2b56e4b0f5", + "sha256:30e9e8bc8c5c17c03d943e8d6f778313efff59e413b8dbdd8214c2ed9aa165f6", + "sha256:324e60bea729cf3b55c1bf9e88fe8b9932c26f8669d13b928e3c96b3a1453dff", + "sha256:37443199f451f8badfe0add666e43cdb817c59fa36bceedafd9c543a42f236ca", + "sha256:47ec0808a8357ab3890ce0eca39a63f79dcf941e2e7f494470fe1c9ec43f6091", + "sha256:496fcc29321e9a804d56d5aa5d7ec1320edfd1898eee2f451aa70171cf1d5a29", + "sha256:50e6c0a17ef7f831b5565fd0394dbf9bfd5d615ee4dd4bb60a3d8c9d2e872323", + "sha256:5527c5475d955c0bc9689c56865aaa2a7b13c504d6c44f0aadbf57b565af5ebd", + "sha256:57d5c7ac62925a8d2ab43ea442b297a56cc8452015e71e24f4aa7e4ed6be3d77", + "sha256:9d45f58b03af1fea4b48e44aa38a819a33dccb9821ef9e1d68f529995f8a632f", + "sha256:b26e2dabda73d347c7af3e6fed58483161c7b87a886a4e06d76ccfe55a044aa9", + "sha256:cfd237865d878da9b65cfee883da5e0067f5e2ff839e459466fb90565a77bda3", + "sha256:d7cca42dba13bfee369e2944ae31f6549a55831cba3117e17636955176004088", + "sha256:fe7de6fed43e7d086e3d947651ec89e55ddf00102f9dd5758763d56d182f0564" + ], + "index": "pypi", + "version": "==1.2.1" + }, + "plotly": { + "hashes": [ + "sha256:7d8aaeed392e82fb8e0e48899f2d3d957b12327f9d38cdd5802bc574a8a39d91", + "sha256:d68fc15fcb49f88db27ab3e0c87110943e65fee02a47f33a8590f541b3042461" + ], + "index": "pypi", + "version": "==4.14.3" + }, "psycopg2-binary": { "hashes": [ - "sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809", - "sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598", - "sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5", - "sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1", - "sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d", - "sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e", - "sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00", - "sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf", - "sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43", - "sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5", - "sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70", - "sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6", - "sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd", - "sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877", - "sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3", - "sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67", - "sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68", - "sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b", - "sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a", - "sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b", - "sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2", - "sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e", - "sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e", - "sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f", - "sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f", - "sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7", - "sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737", - "sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7" + "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", + "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", + "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", + "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", + "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", + "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", + "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", + "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", + "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", + "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", + "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", + "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", + "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", + "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", + "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", + "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", + "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", + "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", + "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", + "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", + "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", + "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", + "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", + "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", + "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", + "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", + "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", + "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", + "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", + "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", + "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", + "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", + "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", + "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", + "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" ], "index": "pypi", - "version": "==2.8.3" + "version": "==2.8.6" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "version": "==2.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.1" }, "python-editor": { "hashes": [ "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", - "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" + "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", + "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", + "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" ], "version": "==1.0.4" }, + "python-graphql-client": { + "hashes": [ + "sha256:951adb2be22ebe4483f87d96009009c22b3f687f4cb109869e0fbfb5aff23f63", + "sha256:a4727d8661c5d5ebe3011bb6160e7b8c2c87299b9cd9d925656d038914469d3f" + ], + "index": "pypi", + "version": "==0.4.2" + }, + "pytz": { + "hashes": [ + "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", + "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" + ], + "version": "==2020.5" + }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" ], "index": "pypi", - "version": "==2.22.0" + "version": "==1.3.0" + }, + "retrying": { + "hashes": [ + "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b" + ], + "version": "==1.3.3" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.12.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "sqlalchemy": { "hashes": [ - "sha256:c30925d60af95443458ebd7525daf791f55762b106049ae71e18f8dd58084c2f" + "sha256:04f995fcbf54e46cddeb4f75ce9dfc17075d6ae04ac23b2bacb44b3bc6f6bf11", + "sha256:0c6406a78a714a540d980a680b86654feadb81c8d0eecb59f3d6c554a4c69f19", + "sha256:0c72b90988be749e04eff0342dcc98c18a14461eb4b2ad59d611b57b31120f90", + "sha256:108580808803c7732f34798eb4a329d45b04c562ed83ee90f09f6a184a42b766", + "sha256:1418f5e71d6081aa1095a1d6b567a562d2761996710bdce9b6e6ba20a03d0864", + "sha256:17610d573e698bf395afbbff946544fbce7c5f4ee77b5bcb1f821b36345fae7a", + "sha256:216ba5b4299c95ed179b58f298bda885a476b16288ab7243e89f29f6aeced7e0", + "sha256:2ff132a379838b1abf83c065be54cef32b47c987aedd06b82fc76476c85225eb", + "sha256:314f5042c0b047438e19401d5f29757a511cfc2f0c40d28047ca0e4c95eabb5b", + "sha256:318b5b727e00662e5fc4b4cd2bf58a5116d7c1b4dd56ffaa7d68f43458a8d1ed", + "sha256:3ab5b44a07b8c562c6dcb7433c6a6c6e03266d19d64f87b3333eda34e3b9936b", + "sha256:426ece890153ccc52cc5151a1a0ed540a5a7825414139bb4c95a868d8da54a52", + "sha256:491fe48adc07d13e020a8b07ef82eefc227003a046809c121bea81d3dbf1832d", + "sha256:4a84c7c7658dd22a33dab2e2aa2d17c18cb004a42388246f2e87cb4085ef2811", + "sha256:54da615e5b92c339e339fe8536cce99fe823b6ed505d4ea344852aefa1c205fb", + "sha256:5a7f224cdb7233182cec2a45d4c633951268d6a9bcedac37abbf79dd07012aea", + "sha256:61628715931f4962e0cdb2a7c87ff39eea320d2aa96bd471a3c293d146f90394", + "sha256:62285607a5264d1f91590abd874d6a498e229d5840669bd7d9f654cfaa599bd0", + "sha256:62fb881ba51dbacba9af9b779211cf9acff3442d4f2993142015b22b3cd1f92a", + "sha256:68428818cf80c60dc04aa0f38da20ad39b28aba4d4d199f949e7d6e04444ea86", + "sha256:6aaa13ee40c4552d5f3a59f543f0db6e31712cc4009ec7385407be4627259d41", + "sha256:70121f0ae48b25ef3e56e477b88cd0b0af0e1f3a53b5554071aa6a93ef378a03", + "sha256:715b34578cc740b743361f7c3e5f584b04b0f1344f45afc4e87fbac4802eb0a0", + "sha256:758fc8c4d6c0336e617f9f6919f9daea3ab6bb9b07005eda9a1a682e24a6cacc", + "sha256:7d4b8de6bb0bc736161cb0bbd95366b11b3eb24dd6b814a143d8375e75af9990", + "sha256:81d8d099a49f83111cce55ec03cc87eef45eec0d90f9842b4fc674f860b857b0", + "sha256:888d5b4b5aeed0d3449de93ea80173653e939e916cc95fe8527079e50235c1d2", + "sha256:95bde07d19c146d608bccb9b16e144ec8f139bcfe7fd72331858698a71c9b4f5", + "sha256:9bf572e4f5aa23f88dd902f10bb103cb5979022a38eec684bfa6d61851173fec", + "sha256:bab5a1e15b9466a25c96cda19139f3beb3e669794373b9ce28c4cf158c6e841d", + "sha256:bd4b1af45fd322dcd1fb2a9195b4f93f570d1a5902a842e3e6051385fac88f9c", + "sha256:bde677047305fe76c7ee3e4492b545e0018918e44141cc154fe39e124e433991", + "sha256:c389d7cc2b821853fb018c85457da3e7941db64f4387720a329bc7ff06a27963", + "sha256:d055ff750fcab69ca4e57b656d9c6ad33682e9b8d564f2fbe667ab95c63591b0", + "sha256:d53f59744b01f1440a1b0973ed2c3a7de204135c593299ee997828aad5191693", + "sha256:f115150cc4361dd46153302a640c7fa1804ac207f9cc356228248e351a8b4676", + "sha256:f1e88b30da8163215eab643962ae9d9252e47b4ea53404f2c4f10f24e70ddc62", + "sha256:f8191fef303025879e6c3548ecd8a95aafc0728c764ab72ec51a0bdf0c91a341" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.3.22" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], - "version": "==1.3.5" + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], - "version": "==1.25.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.2" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==8.1" }, "werkzeug": { "hashes": [ - "sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4", - "sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6" + "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", + "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.0.1" + }, + "yarl": { + "hashes": [ + "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", + "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", + "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", + "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", + "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", + "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", + "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", + "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", + "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", + "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", + "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", + "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", + "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", + "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", + "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", + "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", + "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", + "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", + "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", + "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", + "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", + "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", + "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", + "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", + "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", + "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", + "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", + "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", + "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", + "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", + "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", + "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", + "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", + "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", + "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", + "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", + "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "version": "==0.15.5" + "markers": "python_version >= '3.6'", + "version": "==1.6.3" } }, "develop": {} diff --git a/app.py b/app.py index fd4d1dc..2cda725 100644 --- a/app.py +++ b/app.py @@ -5,20 +5,25 @@ from flask import Flask, render_template, request, redirect, send_from_directory, url_for from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import IntegrityError -from cardcalc import cardcalc, get_last_fight_id, CardCalcException + +from fflogsapi import decompose_url, get_bearer_token +from cardcalc_data import CardCalcException +from cardcalc import cardcalc app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL'] app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) -LAST_CALC_DATE = datetime.fromtimestamp(1563736200) +LAST_CALC_DATE = datetime.fromtimestamp(1611676663) + +token = get_bearer_token() class Report(db.Model): report_id = db.Column(db.String(16), primary_key=True) fight_id = db.Column(db.Integer, primary_key=True) results = db.Column(db.JSON) - friends = db.Column(db.JSON) + actors = db.Column(db.JSON) enc_name = db.Column(db.String(64)) enc_time = db.Column(db.String(9)) enc_kill = db.Column(db.Boolean) @@ -28,26 +33,9 @@ class Count(db.Model): count_id = db.Column(db.Integer, primary_key=True) total_reports = db.Column(db.Integer) -def decompose_url(url): - parts = urlparse(url) - - report_id = [segment for segment in parts.path.split('/') if segment][-1] - try: - fight_id = parse_qs(parts.fragment)['fight'][0] - except KeyError: - raise CardCalcException("Fight ID is required. Select a fight first") - - if fight_id == 'last': - fight_id = get_last_fight_id(report_id) - - fight_id = int(fight_id) - - return report_id, fight_id - def increment_count(db): count = Count.query.get(1) - # TODO: Fix this try: count.total_reports = count.total_reports + 1 @@ -73,7 +61,7 @@ def homepage(): if request.method == 'POST': report_url = request.form['report_url'] try: - report_id, fight_id = decompose_url(report_url) + report_id, fight_id = decompose_url(report_url, token) except CardCalcException as exception: return render_template('error.html', exception=exception) @@ -120,12 +108,12 @@ def calc(report_id, fight_id): # Recompute if no computed timestamp if not report.computed or report.computed < LAST_CALC_DATE: try: - results, friends, encounter_info, cards = cardcalc(report_id, fight_id) + results, actors, encounter_info = cardcalc(report_id, fight_id, token) except CardCalcException as exception: return render_template('error.html', exception=exception) report.results = results - report.friends = friends + report.actors = actors report.enc_name = encounter_info['enc_name'] report.enc_time = encounter_info['enc_time'] report.enc_kill = encounter_info['enc_kill'] @@ -133,19 +121,21 @@ def calc(report_id, fight_id): db.session.commit() + # TODO: this is gonna cause some issues # These get returned with string keys, so have to massage it some - friends = {int(k):v for k,v in report.friends.items()} + actors = {int(k):v for k,v in report.actors.items()} else: try: - results, friends, encounter_info, cards = cardcalc(report_id, fight_id) + results, actors, encounter_info = cardcalc(report_id, fight_id, token) except CardCalcException as exception: return render_template('error.html', exception=exception) + report = Report( report_id=report_id, fight_id=fight_id, results=results, - friends=friends, + actors=actors, **encounter_info ) try: @@ -164,4 +154,4 @@ def calc(report_id, fight_id): # in which case we don't need to do anything besides redirect pass - return render_template('calc.html', report=report, friends=friends) + return render_template('calc.html', report=report, actors=actors) diff --git a/cardcalc.py b/cardcalc.py index 7d713a7..67dcb0e 100644 --- a/cardcalc.py +++ b/cardcalc.py @@ -14,473 +14,69 @@ from datetime import timedelta import os +import pandas as pd -# Make sure we have the requests library -try: - import requests -except ImportError: - raise ImportError("FFlogs parsing requires the Requests module for python." - "Run the following to install it:\n python -m pip install requests") +from cardcalc_data import Player, Pet, CardPlay, BurstWindow, DrawWindow, FightInfo, BurstDamageCollection, CardCalcException, ActorList, SearchWindow -class CardCalcException(Exception): - pass +from fflogsapi import get_card_draw_events, get_card_play_events, get_actor_lists, get_fight_info, get_damage_events -def fflogs_fetch(api_url, options): - """ - Gets a url and handles any API errors - """ - # for now hard card the api key - options['api_key'] = os.environ['FFLOGS_API_KEY'] - options['translate'] = True - - response = requests.get(api_url, params=options) - - # Handle non-JSON response - try: - response_dict = response.json() - except: - raise CardCalcException('Could not parse response: ' + response.text) - - # Handle bad request - if response.status_code != 200: - if 'error' in response_dict: - raise CardCalcException('FFLogs error: ' + response_dict['error']) - else: - raise CardCalcException('Unexpected FFLogs response code: ' + response.status_code) - - return response_dict - -def fflogs_api(call, report, options={}): - """ - Makes a call to the FFLogs API and returns a dictionary - """ - if call not in ['fights', 'events/summary', 'tables/damage-done']: - return {} - - api_url = 'https://www.fflogs.com:443/v1/report/{}/{}'.format(call, report) - - data = fflogs_fetch(api_url, options) - - # If this is a fight list, we're done already - if call in ['fights', 'tables/damage-done']: - return data - - # If this is events, there might be more. Fetch until we have all of it - while 'nextPageTimestamp' in data: - # Set the new start time - options['start'] = data['nextPageTimestamp'] - - # Get the extra data - more_data = fflogs_fetch(api_url, options) - - # Add the new events to the existing data - data['events'].extend(more_data['events']) - - # Continue the loop if there's more - if 'nextPageTimestamp' in more_data: - data['nextPageTimestamp'] = more_data['nextPageTimestamp'] - else: - del data['nextPageTimestamp'] - break - - # Return the event data - return data +from damagecalc import calculate_tick_snapshot_damage, calculate_total_damage, search_burst_window, remove_card_damage """ -# Cards: -# -# Melee: -# Lord of Crowns id#1001876 -# The Balance id#1001882 -# The Arrow id#1001884 -# The Spear id#1001885 -# -# Ranged: -# Lady of Crowns id#1001877 -# The Bole id#1001883 -# The Ewer id#1001886 -# The Spire id#1001887 +For the initial version of this the following simple rules are use. +Every event starts with one of the following and ends with the same: + (1) Draw + (2) Sleeve Draw + (3) Divination +Redraws and plays are ignored """ +def get_draw_windows(card_events, start_time, end_time): -def card_type(guid): - return { - 1001876: 'melee', - 1001877: 'ranged', - 1001882: 'melee', - 1001884: 'melee', - 1001885: 'melee', - 1001883: 'ranged', - 1001886: 'ranged', - 1001887: 'ranged', - } [guid] - -def card_name(guid): - return { - 1001876: 'Lord of Crowns', - 1001877: 'Lady of Crowns', - 1001882: 'The Balance', - 1001884: 'The Arrow', - 1001885: 'The Spear', - 1001883: 'The Bole', - 1001886: 'The Ewer', - 1001887: 'The Spire', - } [guid] - -def card_bonus(guid): - return { - 1001876: 1.08, - 1001877: 1.08, - 1001882: 1.06, - 1001884: 1.06, - 1001885: 1.06, - 1001883: 1.06, - 1001886: 1.06, - 1001887: 1.06, - } [guid] - -def get_draws_divinations(report, start, end): - """ - Gets a list of the card draw events - """ - # x card drawn buffs - #'filter': 'ability.id in (1000915, 1000913, 1000914, 1000917, 1000916, 1000918)', - options = { - 'start': start, - 'end': end, - 'filter': 'ability.id in (3590, 7448, 3593, 16552, 1000915, 1000913, 1000914, 1000917, 1000916, 1000918)', - } - - event_data = fflogs_api('events/summary', report, options) - - draws = [] - - for event in event_data['events']: - # if applybuff then create/modify event with the - # card drawn - if event['type'] == 'applybuff': - draw_set = [draw - for draw in draws - if draw['source'] == event['sourceID'] - and draw['time'] == event['timestamp'] - and 'card' not in draw] - if draw_set: - draw = draw_set[0] - draw['card'] = event['ability']['name'] - draw['id'] = event['ability']['guid'] - else: - draws.append({ - 'source': event['sourceID'], - 'time': event['timestamp'], - 'card': event['ability']['name'], - 'id': event['ability']['guid'], - }) - # if cast then create/modify even with the draw type - # from (draw, redraw, sleevedraw) - elif event['type'] == 'cast' and event['ability']['name'] != 'Divination': - draw_set = [draw - for draw in draws - if draw['source'] == event['sourceID'] - and draw['time'] == event['timestamp'] - and 'type' not in draw] - if draw_set: - draw = draw_set[0] - draw['type'] = event['ability']['name'] - else: - draws.append({ - 'source': event['sourceID'], - 'time': event['timestamp'], - 'type': event['ability']['name'], - }) - - divinations = [] - for event in event_data: - if event['ability']['name'] == 'Divination': - divinations.append({ - 'source': event['sourceID'], - 'time': event['timestamp'], - 'type': event['ability']['name'], - }) + last_time = start_time + last_event = DrawWindow.GetName(0) + current_source = 0 + draw_windows = [] - return (draws, divinations) + for event in card_events: + # check if cast and if it's draw/sleeve/div + if event['type'] == 'cast' and event['abilityGameID'] in [3590, 16552, 7448]: + current_source = event['sourceID'] + draw_windows.append(DrawWindow(current_source, last_time, event['timestamp'], last_event, DrawWindow.GetName(event['abilityGameID']))) -def get_cards_played(report, start, end): - """ - Gets a list of cards played - """ - options = { - 'start': start, - 'end': end, - 'filter': 'ability.id in (1001877, 1001883, 1001886, 1001887, 1001876, 1001882, 1001884, 1001885)' - } - - # print('API Call: https://www.fflogs.com:443/v1/report/{}/{}'.format('events/summary',report)) - # print('Start: {}'.format(options['start'])) - # print('End: {}'.format(options['end'])) - # print('Filter: {}'.format(options['filter'])) + last_time = event['timestamp'] + last_event = DrawWindow.GetName(event['abilityGameID']) - event_data = fflogs_api('events/summary', report, options) + draw_windows.append(DrawWindow(current_source, last_time, end_time, last_event, DrawWindow.GetName(-1))) + return draw_windows + +def get_cards_played(card_events, start_time, end_time): cards = [] # Build list from events - for event in event_data['events']: - # If applying the buff, add an item to the tethers + for event in card_events: + # If applying the buff, add an item to the list of + # cards played if event['type'] == 'applybuff': - cards.append({ - 'source': event['sourceID'], - 'target': event['targetID'], - 'start': event['timestamp'], - 'type': card_type(event['ability']['guid']), - 'name': card_name(event['ability']['guid']), - 'bonus': card_bonus(event['ability']['guid']), - 'id': event['ability']['guid'], - }) + cards.append(CardPlay(event['timestamp'], None, event['sourceID'], event['targetID'], event['abilityGameID'])) # If removing the buff, add an end timestamp to the matching application elif event['type'] == 'removebuff': card_set = [card for card in cards - if card['target'] == event['targetID'] and card['source'] == event['sourceID'] and card['id'] == event['ability']['guid'] and 'end' not in card] + if card.target == event['targetID'] and card.source == event['sourceID'] and card.id == event['abilityGameID'] and card.end is None] # add it to the discovered tether if card_set: card = card_set[0] - card['end'] = event['timestamp'] + card.end = event['timestamp'] # if there is no start event, add one and set it to 15s prior else: - cards.append({ - 'source': event['sourceID'], - 'target': event['targetID'], - 'start': max(event['timestamp'] - 15000, start), - 'end': event['timestamp'], - 'type': card_type(event['ability']['guid']), - 'name': card_name(event['ability']['guid']), - 'bonus': card_bonus(event['ability']['guid']), - 'id': event['ability']['guid'], - }) + cards.append(CardPlay(max(event['timestamp'] - 15000, start_time), event['timestamp'], event['sourceID'], event['targetID'], event['abilityGameID'])) for card in cards: - if 'end' not in card: - # print('Card is missing end') - card['end'] = min(card['start'] + 15000, end) + if card.end is None: + card.end = min(card.start + 15000, end_time) return cards -def get_damages(report, start, end): - """ - Gets non-tick, non-pet damage caused between start and end - """ - options = { - 'start': start, - 'end': end, - 'filter': 'isTick="false"' - } - - damage_data = fflogs_api('tables/damage-done', report, options) - - damages = {} - - for damage in damage_data['entries']: - damages[damage['id']] = damage['total'] - - return damages - -def get_tick_damages(report, version, start, end): - """ - Gets the damage each player caused between start and - end from tick damage that was snapshotted in the - start-end window - """ - # Set up initial options to count ticks - options = { - 'start': start, - 'end': end + 60000, # 60s is the longest dot - 'filter': """ - ability.id not in (1000493, 1000819, 1000820, 1001203, 1000821, 1000140, 1001195, 1001291, 1001221) - and ( - ( - type="applydebuff" or type="refreshdebuff" or type="removedebuff" - ) or ( - isTick="true" and - type="damage" and - target.disposition="enemy" and - ability.name!="Combined DoTs" - ) or ( - ( - type="applybuff" or type="refreshbuff" or type="removebuff" - ) and ( - ability.id=1000190 or ability.id=1000749 or ability.id=1000501 or ability.id=1001205 - ) - ) or ( - type="damage" and ability.id=799 - ) - ) - """ - # Filter explanation: - # 1. exclude non-dot debuff events like foe req that spam event log to minimize requests - # 2. include debuff events - # 3. include individual dot ticks on enemy - # 4. include only buffs corresponding to ground effect dots - # 5. include radiant shield damage - } - - tick_data = fflogs_api('events/summary', report, options) - - # Active debuff window. These will be the debuffs whose damage will count, because they - # were applied within the tether window. List of tuples (sourceID, abilityID) - active_debuffs = [] - - # These will be how much tick damage was applied by a source, only counting - # debuffs applied during the window - tick_damage = {} - - # Wildfire instances. These get special handling afterwards, for stormblood logs - wildfires = {} - - for event in tick_data['events']: - # Fix rare issue where full source is reported instead of just sourceID - if 'sourceID' not in event and 'source' in event and 'id' in event['source']: - event['sourceID'] = event['source']['id'] - - action = (event['sourceID'], event['ability']['guid']) - - # Record wildfires but skip processing for now. Only for stormblood logs - if event['ability']['guid'] == 1000861 and version < 20: - if event['sourceID'] in wildfires: - wildfire = wildfires[event['sourceID']] - else: - wildfire = {} - - if event['type'] == 'applydebuff': - if 'start' not in wildfire: - wildfire['start'] = event['timestamp'] - elif event['type'] == 'removedebuff': - if 'end' not in wildfire: - # Effective WF duration is 9.25 - wildfire['end'] = event['timestamp'] - 750 - elif event['type'] == 'damage': - if 'damage' not in wildfire: - wildfire['damage'] = event['amount'] - - wildfire['target'] = event['targetID'] - - wildfires[event['sourceID']] = wildfire - continue - - # Debuff applications inside window - if event['type'] in ['applydebuff', 'refreshdebuff', 'applybuff', 'refreshbuff'] and event['timestamp'] < end: - # Add to active if not present - if action not in active_debuffs: - active_debuffs.append(action) - - # Debuff applications outside window - elif event['type'] in ['applydebuff', 'refreshdebuff', 'applybuff', 'refreshbuff'] and event['timestamp'] > end: - # Remove from active if present - if action in active_debuffs: - active_debuffs.remove(action) - - # Debuff fades don't have to be removed. Wildfire (ShB) will - # occasionally log its tick damage after the fade event, so faded - # debuffs that deal damage should still be included as implicitly - # belonging to the last application - - # Damage tick - elif event['type'] == 'damage': - # If this is radiant shield, add to the supportID - if action[1] == 799 and event['timestamp'] < end: - if event['supportID'] in tick_damage: - tick_damage[event['supportID']] += event['amount'] - else: - tick_damage[event['supportID']] = event['amount'] - - # Add damage only if it's from a snapshotted debuff - elif action in active_debuffs: - if event['sourceID'] in tick_damage: - tick_damage[event['sourceID']] += event['amount'] - else: - tick_damage[event['sourceID']] = event['amount'] - - # Wildfire handling. This part is hard - # There will be no wildfires for shadowbringers logs, since they are handled - # as a normal DoT tick. - for source, wildfire in wildfires.items(): - # If wildfire never went off, set to 0 damage - if 'damage' not in wildfire: - wildfire['damage'] = 0 - - # If entirely within the window, just add the real value - if ('start' in wildfire and - 'end' in wildfire and - wildfire['start'] > start and - wildfire['end'] < end): - if source in tick_damage: - tick_damage[source] += wildfire['damage'] - else: - tick_damage[source] = wildfire['damage'] - - # If it started after the window, ignore it - elif 'start' in wildfire and wildfire['start'] > end: - pass - - # If it's only partially in the window, calculate how much damage tether would've affected - # Shoutout to [Odin] Lynn Nuvestrahl for explaining wildfire mechanics to me - elif 'end' in wildfire: - # If wildfire started before dragon sight, the start will be tether start - if 'start' not in wildfire: - wildfire['start'] = start - # If wildfire ended after dragon sight, the end will be tether end - if wildfire['end'] > end: - wildfire['end'] = end - - # Set up query for applicable mch damage - options['start'] = wildfire['start'] - options['end'] = wildfire['end'] - - # Only damage on the WF target by the player, not the turret - options['filter'] = 'source.type!="pet"' - options['filter'] += ' and source.id=' + str(source) - options['filter'] += ' and target.id=' + str(wildfire['target']) - - wildfire_data = fflogs_api('tables/damage-done', report, options) - - # If there's 0 damage there won't be any entries - if not len(wildfire_data['entries']): - pass - - # Filter is strict enough that we can just use the number directly - elif source in tick_damage: - tick_damage[source] += int(0.25 * wildfire_data['entries'][0]['total']) - else: - tick_damage[source] = int(0.25 * wildfire_data['entries'][0]['total']) - - return tick_damage - -def get_real_damages(damages, tick_damages, pets): - """ - Combines the two arguments, since cards work with pet damage - this also needs to add in the tick damage from pets - """ - real_damages = {} - for source in damages.keys(): - if source in tick_damages: - real_damages[source] = damages[source] + tick_damages[source] - else: - real_damages[source] = damages[source] - - # search through pets for those owned by anyone in the damage - # sources (this isn't elegant but it works for now) - for pet in pets: - if pets[pet]['petOwner'] in damages.keys() and pet in tick_damages: - real_damages[pets[pet]['petOwner']] += tick_damages[pet] - - return real_damages - -def get_blocked_damage_totals(report, start, end, interval=1, duration=15): - """ - Okay, here's the really complicated and slow process - - I want to go from the start of the fight to the end of the fight in some interval size (default: 1) and check how much damage would be snapshot for a buff of a given duration (default: 15) if played at the start of that interval - - Then combine all of these values for each actor so this information can be parsed/plotted/etc (I don't know, this is gonna be a massive amount of data parsing and ultimately I can't afford to actually make this many API requests so I'm gonna need to grab the whole fight at once and slowly parse it?????) - """ - def print_results(results, friends, encounter_info): """ Prints the results of the tether calculations @@ -526,198 +122,276 @@ def print_results(results, friends, encounter_info): )) print() -def job_type(job_name): - if job_name in {'DarkKnight', 'Gunbreaker', 'Warrior','Paladin', - 'Dragoon', 'Samurai', 'Ninja', 'Monk'}: - return 'melee' - if job_name in {'Machinist', 'Dancer', 'Bard', 'WhiteMage', 'Scholar', 'Astrologian', 'Summoner', 'BlackMage', 'RedMage'}: - return 'ranged' - return 'n/a' +""" +TODO: this might just need to rewritten mostly from scratch because of all the +changes I've made to the backend it's interacting with +""" -def cardcalc(report, fight_id): +def cardcalc(report, fight_id, token): """ Reads an FFLogs report and solves for optimal Card Usage """ + # get fight info + fight_info = get_fight_info(report, fight_id, token) + + # get actors + actors = get_actor_lists(fight_info, token) - report_data = fflogs_api('fights', report) - - version = report_data['logVersion'] - - fight = [fight for fight in report_data['fights'] if fight['id'] == fight_id][0] - - if not fight: - raise CardCalcException("Fight ID not found in report") - - encounter_start = fight['start_time'] - encounter_end = fight['end_time'] - - encounter_timing = timedelta(milliseconds=fight['end_time']-fight['start_time']) + # actors.PrintPlayers() + # actors.PrintPets() - encounter_info = { - 'enc_name': fight['name'], - 'enc_time': str(encounter_timing)[2:11], - 'enc_kill': fight['kill'] if 'kill' in fight else False, - # 'enc_dur': int(encounter_timing.total_seconds()), - } + # Build the list of card plays and draw windows + card_events = get_card_play_events(fight_info, token) + draw_events = get_card_draw_events(fight_info, token) - friends = {friend['id']: friend for friend in report_data['friendlies']} - pets = {pet['id']: pet for pet in report_data['friendlyPets']} + cards = get_cards_played(card_events, fight_info.start, fight_info.end) + draws = get_draw_windows(draw_events, fight_info.start, fight_info.end) + + # Get all damage event and then sort out tick event into snapshot damage events + damage_events = get_damage_events(fight_info, token) + damage_report = calculate_tick_snapshot_damage(damage_events) - # Build the list of tether timings - cards = get_cards_played(report, encounter_start, encounter_end) + non_card_damage_report = remove_card_damage(damage_report, cards, actors) if not cards: raise CardCalcException("No cards played in fight") + if not draws: + raise CardCalcException("No draw events in fight") - results = [] - - # remove cards given to pets since the owner damage includes that - for card in cards: - if card['target'] in pets: - # print('Removed pet with ID: {}'.format(card['target'])) - cards.remove(card) - + # remove cards given to pets since the owner's card will account for that for card in cards: - # Easy part: non-dot damage done in window - damages = get_damages(report, card['start'], card['end']) - - # Hard part: snapshotted dot ticks, including wildfire for logVersion <20 - tick_damages = get_tick_damages(report, version, card['start'], card['end']) - - # Pet Tick damage needs to be added to the owner tick damage - # TODO: I think there's a better way to handle this but this - # works for now - # for tick in tick_damages: - # if tick in pets: - # if pets[tick]['petOwner'] in tick_damages: - # tick_damages[pets[tick]['petOwner']] += tick_damages[tick] - # else: - # tick_damages[pets[tick]['petOwner']] = tick_damages[tick] - - # Combine the two - real_damages = get_real_damages(damages, tick_damages, pets) - - # check the type of card and the type of person who received it - mult = 0 - correct_type = False - - if not (card['target'] in friends): - # print('Another pet found, ID: {}'.format(card['target'])) + if card.target not in actors.players: cards.remove(card) - continue - if job_type(friends[card['target']]['type']) == card['type']: - mult = card['bonus'] - correct_type = True - else: - mult = 1 + ((mult-1.0)/2.0) - correct_type = False - - # Correct damage by removing card bonus from player with card - if card['target'] in real_damages: - real_damages[card['target']] = int(real_damages[card['target']] / mult) - - damage_list = sorted(real_damages.items(), key=lambda dmg: dmg[1], reverse=True) - - # correct possible damage from jobs with incorrect - # type by dividing their 'available' damage in half - # - # also checks if anyone in the list already has a card - # and makes a note of it (the damage bonus from that card - # can't be properly negated but this allows the user to - # ignore that individual or at least swap the card usages - # if the damage difference is large enough between the two - # windows) - corrected_damage = [] + # go through each draw windows and calculate the following + # (1.) Find the card played during this window and get the damage dealt by + # each player during that play window + # (2.) Remove damage bonuses from any active cards during the current + # window + # (3.) Loop through possible play windows form the start of the draw + # window + # to the end in 1s increments and calculate damage done + # (4.) Return the following: + # (a) table of players/damage done in play window + # (b) table of top damage windows + # i. include top 3/5/8/10 for draw window lasting at least + # 0/4/10/20 seconds + # ii. don't include the same player twice in the same 4s interval + # (c) start/end time of draw window + # (d) start/end events of draw window + # (e) card play time (if present) + # (f) source/target + # (g) correct target in play window + # (h) card played + + cardcalc_data = [] + count = 0 + for draw in draws: + count += 1 + # find if there was a card played in this window + card = None + for c in cards: + if c.start > draw.start and c.start < draw.end: + card = c + break - active_cards = [prev_card for prev_card in cards - if prev_card['start'] < card['start'] - and prev_card['end'] > card['start']] - - for damage in damage_list: - mod_dmg = 0 - - has_card = 'No' - for prev_card in active_cards: - if prev_card['start'] < card['start'] and prev_card['end'] > card['start'] and prev_card['target'] == damage[0]: - has_card = 'Yes' - - if card['type'] == job_type(friends[damage[0]]['type']): - mod_dmg = damage[1] + # print(draw) + + # only handle the play window if there was a card played + card_play_data = {} + if card is not None: + # print(card) + # compute damage done during card play window + # print('\tComputing card play damage...') + (_, damages, _) = calculate_total_damage(damage_report, card.start, card.end, actors) + # print('\tDone.') + # check what multiplier should be used to remove the damage bonus + mult = 0 + if actors.players[card.target].role == card.role: + mult = card.bonus else: - mod_dmg = int(damage[1]/2.0) - corrected_damage.append({ - 'id': damage[0], - 'damage': mod_dmg, - 'rawdamage': damage[1], - 'jobtype': job_type(friends[damage[0]]['type']), - 'bonus': int(mod_dmg * (card['bonus'] - 1)), - 'prevcard': has_card, - }) - - corrected_damage_list = sorted(corrected_damage, key=lambda dmg: dmg['damage'], reverse=True) - - # Add to results - timing = timedelta(milliseconds=card['start']-encounter_start) + mult = 1 + ((card.bonus-1.0)/2.0) + + # Correct damage by removing card bonus from player with card + if card.target in damages: + damages[card.target] = int(damages[card.target] / mult) + + # now adjust the damage for incorrect roles + corrected_damage = [] + active_cards = [prev_card for prev_card in cards + if prev_card.start < card.start + and prev_card.end > card.start] + + for pid, dmg in damages.items(): + mod_dmg = dmg + has_card = 'No' + for prev_card in active_cards: + if prev_card.start < card.start and prev_card.end > card.start and prev_card.target == pid: + has_card = 'Yes' + + if card.role != actors.players[pid].role: + mod_dmg = int(dmg/2) + + corrected_damage.append({ + 'id': pid, + 'hasCard': has_card, + 'realDamage': dmg, + 'adjustedDamage': mod_dmg, + 'role': actors.players[pid].role, + 'job': actors.players[pid].job, + }) - # Determine the correct target, the top non-self non-limit combatant - for top in corrected_damage_list: - if friends[top['id']]['type'] != 'LimitBreak' and friends[top['id']]['type'] != 'Limit Break' and top['prevcard'] == 'No': - correct = friends[top['id']]['name'] - break + # convert to dataframe + card_damage_table = pd.DataFrame(corrected_damage) + card_damage_table.set_index('id', inplace=True, drop=False) + card_damage_table.sort_values(by='adjustedDamage', ascending=False, inplace=True) + # get the highest damage target that isn't LimitBreak + optimal_target = card_damage_table[card_damage_table['role'] != 'LimitBreak']['adjustedDamage'].idxmax() - if not correct: - correct = 'Nobody?' + if optimal_target is None: + optimal_target = 'Nobody?' + else: + optimal_target = actors.players[optimal_target].name + + correct = False + if optimal_target == actors.players[card.target].name: + correct = True + + card_play_data = { + 'cardPlayTime': fight_info.ToString(time=card.start), + 'cardDuration': timedelta(milliseconds=card.end-card.start).total_seconds(), + 'cardPlayed': card.name, + 'cardSource': card.source, + 'cardTarget': card.target, + 'cardDamageTable': card_damage_table.to_dict(orient='records'), + 'cardOptimalTarget': optimal_target, + 'cardCorrect': correct, + } + else: + card_play_data = { + 'cardPlayTime': 0, + 'cardTiming': 'N/A', + 'cardDuration': 0, + 'cardPlayed': 'None', + 'cardSource': 0, + 'cardTarget': 0, + 'cardDamageTable': None, + 'cardOptimalTarget': 0, + 'cardCorrect': False, + } - results.append({ - 'damages': corrected_damage_list, - 'timing': str(timing)[2:11], - 'duration': timedelta(milliseconds=card['end']-card['start']).total_seconds(), - 'source': card['source'], - 'target': card['target'], - 'card': card['name'], - 'cardtype': card['type'], - 'correct': correct, - 'correctType': correct_type, - }) + # now we can begin compiling data for the draw window as a whole + card_draw_data = {} + + # check for any cards that are active during the current search window + active_cards = [] + for c in cards: + if c.end > draw.start or c.start < draw.end: + active_cards.append(c) + + # creates a search window from the start of the draw window to the end + # with a 15s duration and 1s step size + search_window = SearchWindow(draw.start, draw.end, 15000, 1000) + # this uses the damage report with all card bonuses removed + draw_window_damage_collection = search_burst_window(non_card_damage_report, search_window, actors) - # results['duration'] = encounter_info['enc_dur'] - return results, friends, encounter_info, cards - -def get_last_fight_id(report): - """Get the last fight in the report""" - report_data = fflogs_api('fights', report) + draw_window_duration = timedelta(milliseconds=(draw.end-draw.start)).total_seconds() + # print('\tDone.') - return report_data['fights'][-1]['id'] + draw_damage = [] -def get_friends_and_pets(report, fight_id): - """ - Reads an FFLogs report and solves for optimal Card Usage - """ - - report_data = fflogs_api('fights', report) - - version = report_data['logVersion'] - - fight = [fight for fight in report_data['fights'] if fight['id'] == fight_id][0] - - if not fight: - raise CardCalcException("Fight ID not found in report") + data_count = 0 + if draw_window_duration < 4.0: + data_count = 3 + elif draw_window_duration < 10.0: + data_count = 5 + elif draw_window_duration < 20.0: + data_count = 8 + else: + data_count = 10 + + # print(draw_window_damage_collection.df) + # print('min: {}'.format(draw_window_damage_collection.df.min().min())) + # print('\tPopulating draw window damage table...') + + (timestamp, pid, damage) = draw_window_damage_collection.GetMax() + collected_count = 1 + draw_damage.append({ + 'count': collected_count, + 'id': pid, + 'damage': damage, + 'timestamp': timestamp, + 'time': fight_info.ToString(time=timestamp)[:5], + }) + # print('\t\tFound: {}'.format(collected_count)) + optimal_time = timestamp + optimal_target = actors.players[pid].name + optimal_damage = damage + + current_damage = damage + while (collected_count < data_count and current_damage > draw_window_damage_collection.df.min().min()): + # get the next lowest damage instance + # print('\t\tCurrent: max value {}'.format(current_damage)) + (time_new, pid_new, damage_new) = draw_window_damage_collection.GetMax(limit=current_damage) + + # update the max damage value we've looked up + current_damage = damage_new + + # if it's the same player in a window that's already + # recorded skip it + ignore_entry = False + for table_entry in draw_damage: + # print('Comparing {}/{} and {}/{}'.format(pid_new, time_new, table_entry['pid'], table_entry['timestamp'])) + if pid_new == table_entry['id'] and abs(time_new - table_entry['timestamp']) < 4000: + ignore_entry = True + + if ignore_entry: + # print('\t\tIgnoring...') + continue + + # if the max damage is 0 then we're done and can exit + if damage_new == 0: + # print('\t\tNo more results to search...') + break - encounter_start = fight['start_time'] - encounter_end = fight['end_time'] + # otherwise we should add the entry to the table + collected_count += 1 + draw_damage.append({ + 'count': collected_count, + 'id': pid_new, + 'damage': damage_new, + 'timestamp': time_new, + 'time': fight_info.ToString(time=time_new)[:5], + }) + # print('\t\tFound: {}'.format(collected_count)) + + draw_damage_table = pd.DataFrame(draw_damage) + draw_damage_table.set_index('id', inplace=True, drop=False) + draw_damage_table.sort_values(by='damage', inplace=True, ascending=False) + + card_draw_data = { + 'startTime': fight_info.ToString(time=draw.start), + 'endTime': fight_info.ToString(time=draw.end), + 'startEvent': draw.startEvent, + 'endEvent': draw.endEvent, + 'drawDamageTable': draw_damage_table.to_dict(orient='records'), + 'drawOptimalTime': fight_info.ToString(time=optimal_time), + 'drawOptimalTarget': optimal_target, + 'drawOptimalDamage': optimal_damage, + 'count': count, + } + + # finally combine the two sets of data and append it to the collection + # of data for each draw window/card play + combined_data = card_draw_data | card_play_data + cardcalc_data.append(combined_data) + # print('\tDone.\n') - encounter_timing = timedelta(milliseconds=fight['end_time']-fight['start_time']) encounter_info = { - 'enc_name': fight['name'], - 'enc_time': str(encounter_timing)[2:11], - 'enc_kill': fight['kill'] if 'kill' in fight else False, - # 'enc_dur': int(encounter_timing.total_seconds()), + 'enc_name': fight_info.name, + 'enc_time': fight_info.ToString(), + 'enc_kill': fight_info.kill, } - - friends = {friend['id']: friend for friend in report_data['friendlies']} - pets = {pet['id']: pet for pet in report_data['friendlyPets']} - - return (friends, pets) \ No newline at end of file + return cardcalc_data, actors.to_dict(), encounter_info diff --git a/cardcalc_data.py b/cardcalc_data.py new file mode 100644 index 0000000..c8e29d0 --- /dev/null +++ b/cardcalc_data.py @@ -0,0 +1,315 @@ +from datetime import timedelta +import pandas as pd +import numpy as np + +class CardCalcException(Exception): + pass + +class Player: + def __init__(self, id, name, job): + self.id = id + self.name = name + self.job = job + self.role = Player.GetRole(job) + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'job': self.job, + 'role': self.role, + 'owner': self.id, + } + + @staticmethod + def GetRole(job): + if job in {'DarkKnight', 'Gunbreaker', 'Warrior','Paladin', 'Dragoon', 'Samurai', 'Ninja', 'Monk'}: + return 'melee' + if job in {'Machinist', 'Dancer', 'Bard', 'WhiteMage', 'Scholar', 'Astrologian', 'Summoner', 'BlackMage', 'RedMage'}: + return 'ranged' + if job in {'LimitBreak', 'Limit Break'}: + return 'LimitBreak' + return 'n/a' + +class Pet: + def __init__(self, id, name, owner): + self.id = id + self.name = name + self.owner = owner + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'role': 'pet', + 'owner': self.owner + } + +class ActorList: + def __init__(self, players: dict, pets: dict): + self.players = players + self.pets = pets + + actors = [] + for _, player in players.items(): + actors.append(player.to_dict()) + + for _, pet in pets.items(): + actors.append(pet.to_dict()) + + self.actors = pd.DataFrame(actors) + self.actors.set_index('id', drop=False, inplace=True) + + def to_dict(self): + return self.actors.to_dict(orient='index') + + def PrintAll(self): + tabular = '{:<24}{:>4} {}' + print('Players') + print(tabular.format('Name','ID','Job')) + print('-'*40) + for _, p in self.players.items(): + print(tabular.format(p.name, p.id, p.job)) + + print('\n') + print('Pets') + print(tabular.format('Name','ID','Owner')) + print('-'*40) + for _, p in self.pets.items(): + print(tabular.format(p.name, p.id, self.players[p.owner].name)) + + def PrintPlayers(self): + tabular = '{:<24}{:>4} {}' + print('Players') + print(tabular.format('Name','ID','Job')) + print('-'*40) + for _, p in self.players.items(): + print(tabular.format(p.name, p.id, p.job)) + + def PrintPets(self): + tabular = '{:<24}{:>4} {:>5} {}' + print('Pets') + print(tabular.format('Name','ID','OID','Owner')) + print('-'*40) + for _, p in self.pets.items(): + print(tabular.format(p.name, p.id, p.owner, self.players[p.owner].name)) + + def GetPlayerID(self, name): + for i, p in self.players.items(): + if p.name == name: + return i + return -1 + +class CardPlay: + def __init__(self, start: int = 0, end: int = 0, source: int = 0, target: int = 0, id: int = 0): + self.start = start + self.end = end + self.source = source + self.target = target + self.id = id + + self.name = CardPlay.GetName(id) + self.role = CardPlay.GetRole(id) + self.bonus = CardPlay.GetBonus(id) + + def __str__(self): + return f'{self.source} played {self.name} on {self.target} at {self.start}' + + def to_dict(self): + return { + 'source': self.source, + 'target': self.target, + 'type': 'play', + 'start': self.start, + 'end': self.end, + 'id': self.id, + 'name': self.name, + 'role': self.role, + 'bonus': self.bonus, + } + + def String(self, player_list, start_time): + return '{} played {} on {} at {}'.format(player_list[self.source]['name'], self.name, player_list[self.target]['name'], str(timedelta(milliseconds=(self.start-start_time)))[2:11]) + + + @staticmethod + def GetName(id): + return { + 1001876: 'Lord of Crowns', + 1001877: 'Lady of Crowns', + 1001882: 'The Balance', + 1001884: 'The Arrow', + 1001885: 'The Spear', + 1001883: 'The Bole', + 1001886: 'The Ewer', + 1001887: 'The Spire', + 0: 'None', + } [id] + + @staticmethod + def GetRole(id): + return { + 1001876: 'melee', + 1001877: 'ranged', + 1001882: 'melee', + 1001884: 'melee', + 1001885: 'melee', + 1001883: 'ranged', + 1001886: 'ranged', + 1001887: 'ranged', + 0: 'none', + } [id] + + @staticmethod + def GetBonus(id): + return { + 1001876: 1.08, + 1001877: 1.08, + 1001882: 1.06, + 1001884: 1.06, + 1001885: 1.06, + 1001883: 1.06, + 1001886: 1.06, + 1001887: 1.06, + 0: 0, + } [id] + +class SearchWindow: + def __init__(self, start, end, duration, step): + self.start = start + self.end = end + self.duration = duration + self.step = step + +class BurstWindow: + def __init__(self, start, end): + self.start = start + self.end = end + +class DrawWindow(BurstWindow): + def __init__(self, source, start, end, startEvent, endEvent): + self.source = source + self.start = start + self.end = end + self.startEvent = startEvent + self.endEvent = endEvent + + def __str__(self): + return f'From {self.startEvent} at {self.start} to {self.endEvent} at {self.end}' + + def to_dict(self): + return { + 'soruce': self.source, + 'type': 'draw', + 'start': self.start, + 'end': self.end, + 'startEvent': self.startEvent, + 'endEvent': self.endEvent + } + + def Duration(self): + return(timedelta(self.end-self.start).total_seconds) + + @staticmethod + def GetName(id): + return { + -1: 'Fight End', + 0: 'Fight Start', + 3590: 'Draw', + 16552: 'Divination', + 7448: 'Sleeve Draw', + 3593: 'Redraw', + }[id] + +class FightInfo: + def __init__(self, report_id, fight_number, start_time, end_time, name, kill): + self.id = report_id + self.index = fight_number + self.start = start_time + self.end = end_time + self.kill = kill + self.name = name + + def to_dict(self): + return { + 'id': self.id, + 'index': self.index, + 'start': self.start, + 'end': self.end, + 'kill': self.kill, + 'name': self.name, + 'duration': self.Duration(), + 'length': self.ToString(), + } + + def Duration(self, time = None): + if time is not None: + return timedelta(milliseconds=(time-self.start)).total_seconds() + else: + return timedelta(milliseconds=(self.end-self.start)).total_seconds() + + def ToString(self, time = None): + if time is not None: + return str(timedelta(milliseconds=(time-self.start)))[2:11] + else: + return str(timedelta(milliseconds=(self.end-self.start)))[2:11] + + def PrintDamageObject(self, actor_list, damage_obj): + format_string = '{:>9} {:<25}...{:>9}' + print(format_string.format(self.TimeElapsed(damage_obj[0]), actor_list.actors.loc[damage_obj[1], 'name'], damage_obj[2] )) + + def TimeElapsed(self, time = None): + if time is not None: + return time-self.start + else: + return self.end-self.start + + def TimeDelta(self, time): + return timedelta(milliseconds=(time - self.start)) + +class BurstDamageCollection: + def __init__(self, df, duration): + self.df = df + self.duration = duration + + # this returns a tuple with the (timestamp, id, damage) set which is the + # max + def GetMax(self, pid=None, time=None, limit=0): + # Options: + # (1) if there is no time and no id then find overall max + # (2) if there is no time but an id then find max for that id + # (3) if a time is specified then check if it's valid and find the + # overall max at that time + # (4) if there is a time and a player then return their damage at that + # timestamp assuming it's valid + + # if a limit is provided (limit > 0) then only search values less than the limit + if limit > 0: + mod_df = self.df.apply(lambda x: [y if y < limit else 0 for y in x]) + else: + mod_df = self.df + + max_dmg = 0 + if time is None and pid is None: + # get overall max damage, person, and time + pid = mod_df.max(axis=0).idxmax() + time = mod_df.max(axis=1).idxmax() + max_dmg = mod_df.loc[time, pid] + elif pid is None and time is not None and time in mod_df.index.values: + # get max damage and the person for this time + pid = mod_df.loc[time, :].idxmax() + max_dmg = mod_df.loc[time, pid] + elif pid is not None and pid in mod_df.columns.values and time is None: + # get the max damage done by this person and at what time + time = mod_df[pid].idxmax() + max_dmg = mod_df.loc[time, pid] + elif pid is not None and pid in mod_df.columns.values and time is not None and time in mod_df.index.values: + # return the damage at time done by the given player + max_dmg = mod_df.loc[time, pid] + else: + # some error + time = 0 + pid = 0 + max_dmg = 0 + + return [int(time), int(pid), int(max_dmg)] \ No newline at end of file diff --git a/damagecalc.py b/damagecalc.py new file mode 100644 index 0000000..3af8a06 --- /dev/null +++ b/damagecalc.py @@ -0,0 +1,216 @@ +from cardcalc_data import Player, Pet, SearchWindow, FightInfo, BurstDamageCollection, ActorList + +import pandas as pd +import numpy as np + +""" +This takes a collection of damage events associated with ticks as well as the +""" +def calculate_tick_snapshot_damage(damage_events): + active_debuffs = {} + summed_tick_damage = [] + + for event in damage_events['tickDamage']: + action = (event['sourceID'], event['targetID'], event['abilityGameID']) + + # these events are either: + # - apply{buff/debuff} + # - reapply{buff,debuff} + # - remove{buff,debuff} (can ignore these) + # - damage + + # damage is summed from the application (apply or reapply) until + # another application event or the end of the data + + # that damage is then reassociated with application event + + if event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp']: + # if it's not an active effect then add it + if action not in active_debuffs: + active_debuffs[action] = { + 'timestamp': event['timestamp'], + 'damage': 0, + } + # if it is an active debuff then add a new damage event associated + # with the sum and restart summing the damage from this event + else: + summed_tick_damage.append({ + 'type': 'damagesnapshot', + 'sourceID': action[0], + 'targetID': action[1], + 'abilityGameID': action[2], + 'amount': active_debuffs[action]['damage'], + 'timestamp': active_debuffs[action]['timestamp'], + }) + active_debuffs[action] = { + 'timestamp': event['timestamp'], + 'damage': 0, + } + elif event['type'] == 'damage': + if action in active_debuffs: + active_debuffs[action]['damage'] += event['amount'] + + # now that we're done we can add the remaining events into the damage array + for action in active_debuffs: + if active_debuffs[action]['damage'] != 0: + summed_tick_damage.append({ + 'type': 'damagesnapshot', + 'sourceID': action[0], + 'targetID': action[1], + 'abilityGameID': action[2], + 'amount': active_debuffs[action]['damage'], + 'timestamp': active_debuffs[action]['timestamp'], + }) + + # finally sort the new array of snapshotdamage events and return it + sorted_tick_damage = sorted(summed_tick_damage, key=lambda tick: tick['timestamp']) + + damage_report = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + + return damage_report + +def calculate_tick_damage(damage_events): + instanced_tick_damage = [] + + for event in damage_events['tickDamage']: + if event['type'] == 'damage': + instanced_tick_damage.append({ + 'timestamp': event['timestamp'], + 'sourceID': event['sourceID'], + 'targetID': event['targetID'], + 'amount': event['amount'], + 'type': 'tickdamage', + 'abilityGameID': event['abilityGameID'] + }) + + # finally sort the new array of snapshotdamage events and return it + sorted_tick_damage = sorted(instanced_tick_damage, key=lambda tick: tick['timestamp']) + + damage_report = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + + return damage_report + +def remove_card_damage(damage_report, cards, actors): + for card in cards: + # check the real bonus received + eff_bonus = 1.0 + + print(card) + + if card.target in actors.players: + if card.role == actors.players[card.target].role: + eff_bonus = card.bonus + else: + eff_bonus = 1.0 + ((card.bonus - 1.0)/2.0) + elif card.target in actors.pets: + if card.role == actors.players[actors.pets[card.target].owner].role: + eff_bonus = card.bonus + else: + eff_bonus = 1.0 + ((card.bonus - 1.0)/2.0) + + # check if there are any valid damage values for the active card holder during it's time window (this should be non-empty but especially for pets may sometimes not be) + if damage_report.loc[lambda df: (df['timestamp'] > card.start) & (df['timestamp'] < card.end) & (df['sourceID'] == card.target), 'amount'].empty: + next + else: + # modifiy all values with the correct sourceID that lie between the start event and end event times for the card + damage_report.loc[lambda df: (df['timestamp'] > card.start) & (df['timestamp'] < card.end) & (df['sourceID'] == card.target), 'amount'] = damage_report.loc[lambda df: (df['timestamp'] > card.start) & (df['timestamp'] < card.end) & (df['sourceID'] == card.target), 'amount'].transform(lambda x: int(x/eff_bonus)) + + return damage_report + +def calculate_total_damage(damage_report, start_time, end_time, actors: ActorList): + combined_damage = {} + + # create a dataframe with only the current time window + current_df = damage_report.query('timestamp >= {} and timestamp <= {}'.format(start_time, end_time)) + + for actor in current_df['sourceID'].unique(): + combined_damage[actor] = current_df.query('sourceID == {}'.format(actor))['amount'].sum() + + player_damage = {} + for p in actors.players: + if p in combined_damage: + player_damage[p] = combined_damage[p] + else: + player_damage[p] = 0 + combined_damage[p] = 0 + + pet_damage = {} + for p in actors.pets: + if p in combined_damage: + pet_damage[p] = combined_damage[p] + if actors.pets[p].owner in player_damage: + player_damage[actors.pets[p].owner] += combined_damage[p] + else: + player_damage[actors.pets[p].owner] = combined_damage[p] + else: + pet_damage[p] = 0 + combined_damage[p] = 0 + + return (combined_damage, player_damage, pet_damage) + +""" +This searches a window of time for the optimal card play + +damage_report: contains all damage instances (both raw and from summing dot snapshots) +start_time: initial value for the search interval to start +end_time: final time that the interval can start +duration: the length of the interval (in milliseconds) +step_size: step_size for the search (in milliseconds) +""" +def search_burst_window(damage_report, search_window: SearchWindow, actors: ActorList): + ### + ### TODO: this function is likely the whole computational time + ### of this project right now so any work to optimize this will + ### greatly aid the performance of this project + ### + # start searching at the start + interval_start = search_window.start + interval_end = interval_start + search_window.duration + + damage_collection = [] + # print('\t\tStarting search in window from {} to {}'.format(search_window.start, search_window.end)) + + while interval_start < search_window.end: + # print('\t\t\tSearching at {}...'.format(interval_start)) + (_, total_damage, _) = calculate_total_damage(damage_report, interval_start, interval_end, actors) + + # add all values to the collection at this timestamp + current_damage = total_damage + current_damage['timestamp'] = interval_start + damage_collection.append(current_damage) + + interval_start += search_window.step + interval_end = interval_start + search_window.duration + # print('\t\t\tDone.') + + damage_df = pd.DataFrame(damage_collection) + damage_df.set_index('timestamp', drop=True, inplace=True) + # print('\t\tDone with full search.') + return BurstDamageCollection(damage_df, search_window.duration) + + +def time_averaged_dps(damage_report, startTime, endTime, stepSize, timeRange): + + average_dps = [] + + current_time = startTime + min_time = max(current_time - timeRange, startTime) + max_time = min(current_time + timeRange, endTime) + + # sum up all + while current_time < endTime: + delta = (max_time - min_time)/1000 + + active_events = damage_report.query('timestamp <= {} and timestamp >= {}'.format(max_time, min_time)) + step_damage = active_events['amount'].sum() + + average_dps.append({ + 'timestamp': current_time, + 'dps': step_damage/delta, + }) + + current_time += stepSize + min_time = max(current_time - timeRange, startTime) + max_time = min(current_time + timeRange, endTime) + + return pd.DataFrame(average_dps) \ No newline at end of file diff --git a/fflogsapi.py b/fflogsapi.py new file mode 100644 index 0000000..feb35e8 --- /dev/null +++ b/fflogsapi.py @@ -0,0 +1,322 @@ +""" +This contains code for pull requests from v2 of the FFLogs API +as required for damage and card calculations used in cardcalc +and damagecalc +""" + +from datetime import timedelta +import os + +# local imports +from cardcalc_data import Player, Pet, FightInfo, CardCalcException, ActorList + +# Imports related to making API requests +# import requests +from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient +from python_graphql_client import GraphqlClient +from urllib.parse import urlparse, parse_qs + +FFLOGS_CLIENT_ID = os.environ['FFLOGS_CLIENT_ID'] +FFLOGS_CLIENT_SECRET = os.environ['FFLOGS_CLIENT_SECRET'] + +FFLOGS_OAUTH_URL = 'https://www.fflogs.com/oauth/token' +FFLOGS_URL = 'https://www.fflogs.com/api/v2/client' + +client = GraphqlClient(FFLOGS_URL) + +# this is used to handle sorting events +def event_priority(event): + return { + 'applydebuff': 1, + 'applybuff': 1, + 'applydebuffstack': 1, + 'refreshdebuff': 2, + 'refreshbuff': 2, + 'removedebuff': 4, + 'removebuff': 4, + 'damage': 3, + 'damagesnapshot': 3, + }[event] + +# used to obtain a bearer token from the fflogs api +def get_bearer_token(): + token_client = BackendApplicationClient(client_id=FFLOGS_CLIENT_ID) + + oauth = OAuth2Session(client=token_client) + token = oauth.fetch_token(token_url=FFLOGS_OAUTH_URL, client_id=FFLOGS_CLIENT_ID, client_secret=FFLOGS_CLIENT_SECRET) + return token + +# make a request for the data defined in query given a set of +# variables +def call_fflogs_api(query, variables, token): + headers = { + 'Content-TYpe': 'application/json', + 'Authorization': 'Bearer {}'.format(token['access_token']), + } + data = client.execute(query=query, variables=variables, headers=headers) + + return data + +def get_last_fight(report, token): + variables = { + 'code': report + } + query = """ +query reportData($code: String!) { + reportData { + report(code: $code) { + fights { + id + startTime + endTime + name + kill + } + } + } +} +""" + data = call_fflogs_api(query, variables, token) + return data['data']['reportData']['report']['fights'][-1]['id'] + +def decompose_url(url, token): + parts = urlparse(url) + + report_id = [segment for segment in parts.path.split('/') if segment][-1] + try: + fight_id = parse_qs(parts.fragment)['fight'][0] + except KeyError: + raise CardCalcException("Fight ID is required. Select a fight first") + + if fight_id == 'last': + fight_id = get_last_fight(report_id, token) + fight_id = int(fight_id) + + return report_id, fight_id + + +def get_fight_info(report, fight, token): + variables = { + 'code': report + } + query = """ +query reportData($code: String!) { + reportData { + report(code: $code) { + fights { + id + startTime + endTime + name + kill + } + } + } +} +""" + data = call_fflogs_api(query, variables, token) + fights = data['data']['reportData']['report']['fights'] + + for f in fights: + if f['id'] == fight: + return FightInfo(report_id=report, fight_number=f['id'], start_time=f['startTime'], end_time=f['endTime'], name=f['name'], kill=f['kill']) + + raise CardCalcException("Fight ID not found in report") + +def get_actor_lists(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float) { + reportData { + report(code: $code) { + masterData { + pets: actors(type: "Pet") { + id + name + type + subType + petOwner + } + } + table: table(startTime: $startTime, endTime: $endTime) + } + } +}""" + + data = call_fflogs_api(query, variables, token) + master_data = data['data']['reportData']['report']['masterData'] + table = data['data']['reportData']['report']['table'] + + pet_list = master_data['pets'] + composition = table['data']['composition'] + + players = {} + pets = {} + + for p in composition: + players[p['id']] = Player(p['id'], p['name'], p['type']) + + for p in pet_list: + if p['petOwner'] in players: + pets[p['id']] = Pet(p['id'], p['name'], p['petOwner']) + + return ActorList(players, pets) + +def get_card_play_events(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + cards: events( + startTime: $startTime, + endTime: $endTime + dataType: Buffs, + filterExpression: "ability.id in (1001877, 1001883, 1001886, 1001887, 1001876, 1001882, 1001884, 1001885)" + ) { + data + } + } + } +} +""" + + data = call_fflogs_api(query, variables, token) + card_events = data['data']['reportData']['report']['cards']['data'] + + return card_events + +def get_card_draw_events(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + draws: events( + startTime: $startTime, + endTime: $endTime, + filterExpression: "ability.id in (3590, 7448, 16552, 3593, 1000915, 1000913, 1000914, 1000917, 1000916, 1000918)" + ) { + data + } + } + } +} +""" + + data = call_fflogs_api(query, variables, token) + card_events = data['data']['reportData']['report']['draws']['data'] + + return card_events + +# this shouldn't be used much but can be useful so I'm leaving it in +def get_damages(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } + query = """ +query reportData ($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + table( + startTime: $startTime, + endTime: $endTime, + dataType: DamageDone, + filterExpression: "isTick='false'", + viewBy: Source + ) + } + } +}""" + + data = call_fflogs_api(query, variables, token) + damage_entries = data['data']['reportData']['report']['table']['data']['entries'] + + damages = {} + + for d in damage_entries: + damages[d['id']] = d['total'] + + return damages + +def get_damage_events(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + damage: events( + startTime: $startTime, + endTime: $endTime, + dataType: DamageDone, + limit: 10000, + filterExpression: "isTick='false' and type!='calculateddamage'" + ) { + data + } + tickDamage: events( + startTime: $startTime, + endTime: $endTime, + dataType: DamageDone, + limit: 10000, + filterExpression: "isTick='true' and ability.id != 500000" + ) { + data + } + tickEvents: events( + startTime: $startTime, + endTime: $endTime, + dataType: Debuffs, + hostilityType: Enemies, + limit: 10000, + filterExpression: "ability.id not in (1000493, 1001203, 1001195, 1001221)" + ) { + data + } + groundEvents: events( + startTime: $startTime, + endTime: $endTime, + dataType: Buffs, + limit: 10000, + filterExpression: "ability.id in (1000749, 1000501, 1001205, 1000312, 1001869)" + ) { + data + } + } + } +} +""" + + data = call_fflogs_api(query, variables, token) + + base_damages = data['data']['reportData']['report']['damage']['data'] + tick_damages = data['data']['reportData']['report']['tickDamage'] ['data'] + tick_events = data['data']['reportData']['report']['tickEvents']['data'] + ground_events = data['data']['reportData']['report']['groundEvents']['data'] + + combined_tick_events = sorted((tick_damages + tick_events + ground_events), key=lambda tick: (tick['timestamp'], event_priority(tick['type']))) + + damage_events = { + 'rawDamage': base_damages, + 'tickDamage': combined_tick_events, + } + return damage_events diff --git a/localtesting.py b/localtesting.py deleted file mode 100644 index a736982..0000000 --- a/localtesting.py +++ /dev/null @@ -1,58 +0,0 @@ -from datetime import datetime -import os -from urllib.parse import urlparse, parse_qs - -from cardcalc import cardcalc, get_last_fight_id, CardCalcException, print_results, get_cards_played, fflogs_api, timedelta, get_friends_and_pets - -LAST_CALC_DATE = datetime.fromtimestamp(1563736200) - -def decompose_url(url): - parts = urlparse(url) - - report_id = [segment for segment in parts.path.split('/') if segment][-1] - try: - fight_id = parse_qs(parts.fragment)['fight'][0] - except KeyError: - raise CardCalcException("Fight ID is required. Select a fight first") - - if fight_id == 'last': - fight_id = get_last_fight_id(report_id) - - fight_id = int(fight_id) - - return report_id, fight_id - - -# local testing here: - -# USE THIS: https://www.fflogs.com/reports/qBxNr4V12gmZz63R#fight=12&type=damage-done - -# Call Order: -# (1) cardcalc - -#zeke's e12s 100: https://www.fflogs.com/reports/r7tnPLDhJb6KYVaf#fight=19&type=damage-done -# report = 'r7tnPLDhJb6KYVaf' -# fight = 19 - -#marielle's recent e9s run: https://www.fflogs.com/reports/byLqHjz8MnphQP3r#fight=1&type=damage-done -# report = 'byLqHjz8MnphQP3r' -# fight = 1 - -#x's e10s #1 parse (1/14/21): https://www.fflogs.com/reports/JkCGX4pqW1N2Fm9h#fight=21&type=damage-done -# report = 'JkCGX4pqW1N2Fm9h' -# fight = 21 - -#zeke's best e10s parse (1/14/21): https://www.fflogs.com/reports/Cpbh94KWTRPtHdam#fight=7&type=damage-done -# report = 'Cpbh94KWTRPtHdam' -# fight = 7 - -#e12p1 pet testing: jyXMVZbC94RB8ADh fight: 25 -report = 'jyXMVZbC94RB8ADh' -fight = 25 - -# overwrite testing: https://www.fflogs.com/reports/Xta1JRmZqDTnjzM7#fight=last -(report, fight) = decompose_url('https://www.fflogs.com/reports/Xta1JRmZqDTnjzM7#fight=last') - -(results, friends, encounter_info, cards) = cardcalc(report, fight) -print_results(results, friends, encounter_info) - diff --git a/plotting.py b/plotting.py new file mode 100644 index 0000000..0a5e2f7 --- /dev/null +++ b/plotting.py @@ -0,0 +1,2 @@ +import plotly.graph_objects as go +import pandas as pd \ No newline at end of file diff --git a/templates/about.html b/templates/about.html index bc4b649..341b224 100644 --- a/templates/about.html +++ b/templates/about.html @@ -3,7 +3,7 @@ {% block content %}

What is this?

-

This is a tool to calculate the optimal target for Astrologian cards in Final Fantasy XIV

+

This is a tool to calculate more optimal targets for Astrologian cards in Final Fantasy XIV. This is broken down into two different searchs. The first looking for the optimal target with each window where a card was actually played in the fight ('Card Play' window). The second search looks at possible times between each Draw/Sleeve Draw/Divination where players were doing the most damage during a 15s window ('Card Draw' window).

Does it account for...

@@ -16,37 +16,75 @@

Does it account for...

  • Includes the portion of Wildfire damage generated by damage inside the tether window (Stormblood logs only)
  • Includes damage from ground effect DoTs (Shadow Flare, Doton, Salted Earth, Flamethrower)
  • Includes damage from Radiant Shield (when applicable)
  • -
  • Excludes the 3%/4%/6%/8% damage buff from the card
  • +
  • Excludes the 3%/4%/6%/8% damage buff from the active card
  • Accounts for the correct melee/ranged bonus associated with the card
  • +
  • During a 'Card Play' window any targets who already have an active card at the time of cast is ignored for recomendation as the optimal target
  • +
  • While searching for optimal cards during a 'Card Draw' window all damage from card buffs is ignored
  • How it works

    -

    This is the methodology used by the script for each tether window

    +

    This is the methodology used by the script for each 'Card Play' window

    1. Get all of the non-tick, direct damage inside the card window (typically 15s) for each player
    2. -
    3. Walk through each debuff and tick event. +
    4. Find the start of all tick application or reapplication events that occur during the card window and then sum the associated damage events (even if those occur outside of the card window)
    5. +
    6. Take out the appropriate bonus to the player that actually receieved the card based on what card was played and the job of the receiving player
    7. +
    + +

    After doing all of that, the result is a fairly accurate representation of the total amount of damage that would have been buffed by the card, if that player were its target at the time it was originally played.

    + +

    Separately for each 'Card Draw' window the script uses the following methodology:

    +
      +
    1. Get all of the non-tick, direct damage inside the card window (typically 15s) for each player
    2. +
    3. For every tick application or reapplication the damage events associated with that event are summed up and listed as a new damage event at the application time with a total amount associated with the summed damage value
    4. +
    5. The damage bonus associated with every card played throughout the fight is removed
    6. +
    7. Starting at the time of the initial event:
    8. - -
    9. Take out the appropriate bonus to the player that actually receieved the card based on what card was played and the job of the receiving player
    -

    After doing all of that, the result is an accurate representation of the total amount of damage that would have been buffed by the card, if that player were its target at the time it was originally played.

    +

    This output should help to accurately predict where it might have been possible to play a card on a player while they are doing the maximum amount. However, it does have some limitations discussed below

    Current limitations

    -

    Currently the analysis of whether the card was played "optimally" is fairly simple. It doesn't do any of the following (yet):

    +

    Some notes on the current analysis with regards to the optimal target for each 'Card Play' window:

    +

    While searching for the maximum damage output available during each potential 'Card Draw' window the following considerations are not taken into account:

    +

    Who made this?

    @@ -54,6 +92,5 @@

    Who made this?

    If you notice any issues, feel free to DM me through Discord (Melody ♫#1653), or open issues and pull requests on the GitHub repo for this site.

    Changes

    -

    2021-01-15: Fixed pet tick damage (such as SMN Ifrit Enkindle DoT) not being correctly included in the damage total for that player. Fixed in git commit 1eafff2

    {% endblock %} diff --git a/templates/base.html b/templates/base.html index 55fc358..de85d25 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,9 @@ margin-bottom: 180px; } } + table, th, td { + padding: 5px; + } .badge { font-size: 100%; } @@ -63,6 +66,99 @@ 100% { transform: rotate(360deg); } } + /* the following is for the tabs */ + .tabset { + margin-top: 15px; + margin-bottom: 15px; + } + + .tabset > input { + display:block; /* "enable" hidden elements in IE/edge */ + position:absolute; /* then hide them off-screen */ + left:-100%; + } + + .tabset > ul { + position:static; + z-index:999; + list-style:none; + display:flex; + margin-bottom:-9px; + margin-left: -40px; + } + + .tabset > ul label, + .tabset > div { + /* border:1px solid hsl(0, 0%, 40%); */ + border:1px solid rgba(29, 49, 44, 0.733); + } + + .tabset > ul label { + display:inline-block; + padding:0.5em 2em; + background:hsl(0, 0%, 100%); + border-right-width:0; + } + + .tabset > ul li:first-child label { + border-radius:0.3em 0 0 0; + } + + .tabset > ul li:last-child label { + border-right-width:1px; + border-radius:0 0.3em 0 0; + } + + .tabset > div { + position:relative; + background:hsl(0, 0%, 100%); + border-radius:0em 0.3em 0.3em 0.3em; + } + + .tabset > input:nth-child(1):checked ~ ul li:nth-child(1) label, + .tabset > input:nth-child(2):checked ~ ul li:nth-child(2) label, + .tabset > input:nth-child(3):checked ~ ul li:nth-child(3) label, + .tabset > input:nth-child(4):checked ~ ul li:nth-child(4) label, + .tabset > input:nth-child(5):checked ~ ul li:nth-child(5) label, + .tabset > input:nth-child(6):checked ~ ul li:nth-child(6) label, + .tabset > input:nth-child(7):checked ~ ul li:nth-child(7) label, + .tabset > input:nth-child(8):checked ~ ul li:nth-child(8) label, + .tabset > input:nth-child(9):checked ~ ul li:nth-child(9) label { + border-bottom-color:hsl(0, 0%, 83%); + background: rgba(120, 194, 173, 0.5) + } + + .tabset > div > section, + .tabset > div > section h2 { + position:absolute; + top:-999em; + left:-999em; + } + .tabset > div > section { + padding:1em 1em 0; + } + + .tabset > input:nth-child(1):checked ~ div > section:nth-child(1), + .tabset > input:nth-child(2):checked ~ div > section:nth-child(2), + .tabset > input:nth-child(3):checked ~ div > section:nth-child(3), + .tabset > input:nth-child(4):checked ~ div > section:nth-child(4), + .tabset > input:nth-child(5):checked ~ div > section:nth-child(5), + .tabset > input:nth-child(6):checked ~ div > section:nth-child(6), + .tabset > input:nth-child(7):checked ~ div > section:nth-child(7), + .tabset > input:nth-child(8):checked ~ div > section:nth-child(8), + .tabset > input:nth-child(9):checked ~ div > section:nth-child(9) { + position:Static; + } + + .tabset > ul label { + -webkit-touch-callout:none; + -webkit-user-select:none; + -khtml-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; + } + diff --git a/templates/calc.html b/templates/calc.html index 9b35c53..8560f7d 100644 --- a/templates/calc.html +++ b/templates/calc.html @@ -8,44 +8,125 @@
    {{ report.enc_time }}
    Original log + {% for result in report.results %} -
    -
    -

    - {{ friends[result['source']]['name'] }} - played - {{ result.card }} - on - {{ friends[result['target']]['name'] }} - at - {{ result.timing }} -

    -

    The correct target was {{ result['correct'] }}

    - - - - - - - - - - - - {% for damage in result['damages'] %} - {% if friends[damage['id']]['type'] != 'LimitBreak' %} - - - - - - +
    + + + +
    +
    +

    Card Play

    +
    + {% if result.cardPlayed != 'None' %} +
    +
    +

    + {{ actors[result['cardSource']]['name'] }} + played + {{ result.cardPlayed }} + on + {{ actors[result['cardTarget']]['name'] }} + at + {{ result.cardPlayTime }} +

    +

    The correct target was {{ result['cardOptimalTarget'] }}

    + +
    PlayerJobAdjusted DamageRaw Damage
    {{ friends[damage['id']]['name'] }}{{ friends[damage['id']]['type'] }}{{ damage['damage'] }}{{ damage['rawdamage'] }}
    + + + + + + + + + + {% for damage in result['cardDamageTable'] %} + {% if actors[damage['id']]['role'] != 'LimitBreak' %} + + + + + + + {% endif %} + {% endfor %} + +
    PlayerJobAdjusted DamageRaw Damage
    {{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['adjustedDamage'] }}{{ damage['realDamage'] }}
    +
    +
    +
    + {% else %} +
    + No card was played in this draw window (likely a result of Divination being cast) +
    {% endif %} - {% endfor %} - - + + + +
    +

    Card Draw

    +
    +
    +
    +

    + From + {{ result['startEvent'] }} + at + {{ result['startTime'] }} + until + {{ result['endEvent'] }} + at + {{ result['endTime'] }} +

    +

    + The optimal target was {{ result['drawOptimalTarget'] }} + at {{ result['drawOptimalTime'] }}. +

    + + + + + + + + + + + + {% for damage in result['drawDamageTable'] %} + + + + + + + {% endfor %} + +
    Play TimePlayerJobDamage
    {{ damage['time'] }}{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['damage'] }}
    +
    +
    +
    + +
    +
    -
    {% endfor %} + {% endblock %} \ No newline at end of file diff --git a/testing.py b/testing.py new file mode 100644 index 0000000..d4f66d0 --- /dev/null +++ b/testing.py @@ -0,0 +1,83 @@ +from datetime import timedelta +import os +from urllib.parse import urlparse, parse_qs + +from fflogsapi import get_bearer_token, get_actor_lists, get_damage_events, get_fight_info, decompose_url +from cardcalc_data import ActorList, FightInfo, SearchWindow, CardCalcException +from damagecalc import search_burst_window, calculate_tick_snapshot_damage, calculate_tick_damage, time_averaged_dps +from cardcalc import cardcalc + +import pandas as pd +import numpy as np + +import plotly.io as pio +import plotly.express as px +import plotly.graph_objects as go + +import scipy.signal as sig +import scipy as scipy + +def test_to_dict_damage_table(card_damage_table): + print(card_damage_table) + +def test_to_dict_actor_list(actor_list): + test_dict = actor_list.to_dict() + print(test_dict) + +def test_plotting(): + # df_base.set_index(pd.TimedeltaIndex(data=df_base['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + # df_snapshot.set_index(pd.TimedeltaIndex(data=df_snapshot['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + + total_ms = fight_info.end - fight_info.start + step_size = int(total_ms/250) + averaging_size = step_size*4 + print('Step: {}\nAveraging: {}'.format(step_size, averaging_size)) + + average_dps = time_averaged_dps(damage_report, fight_info.start, fight_info.end, step_size, averaging_size) + base_average_dps = time_averaged_dps(damage_report_base, fight_info.start, fight_info.end, step_size, averaging_size) + + average_dps.set_index(pd.TimedeltaIndex(data=average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + base_average_dps.set_index(pd.TimedeltaIndex(data=base_average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + + average_dps.index = average_dps.index + pd.Timestamp("1970/01/01") + base_average_dps.index = base_average_dps.index + pd.Timestamp("1970/01/01") + + fig = go.Figure() + + fig.add_trace(go.Scatter(name='Snapshot DPS', x=average_dps.index, y=average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) + + fig.add_trace(go.Scatter(name='Base DPS', x=base_average_dps.index, y=base_average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) + + # fig.update_layout(template='plotly_white') + fig.update_layout(title='Damage Done') + + fig.update_layout(xaxis = dict(tickformat = '%M:%S', nticks=20)) + + fig.update_layout(yaxis_range=[0,max(average_dps['dps'].max(), base_average_dps['dps'].max())*1.05]) + fig.show() + + # pio.write_html(fig, file='index.html', auto_open=True) + +token = get_bearer_token() + +# url = 'https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1' +# url = 'https://www.fflogs.com/reports/KaCwVdgTQYhmRAxD#fight=10' +# url = 'https://www.fflogs.com/reports/byLqHjz8MnphQP3r#fight=1' +# url = 'https://www.fflogs.com/reports/TmzFDHfWL8bhdMAn#fight=6' +# url = 'https://www.fflogs.com/reports/fZXhDbTjw7GWmKLz#fight=2' +url = 'https://www.fflogs.com/reports/p47GRHQBvaZXq1xk#fight=last' + +report_id, fight_id = decompose_url(url, token) + +fight_info = get_fight_info(report_id, fight_id, token) +actor_list = get_actor_lists(fight_info, token) + +damage_data = get_damage_events(fight_info, token) + +damage_report = calculate_tick_snapshot_damage(damage_data) +print(damage_report.loc[lambda df: df['timestamp'] > 1489968]) +print('Intentionally Empty') +print(damage_report.loc[lambda df: (df['timestamp'] > 1489968) & (df['sourceID'] == 3)]) + +cardcalc_data, actors, _ = cardcalc(report_id, fight_id, token) +print(cardcalc_data)