diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/card_creation.py b/app/card_creation.py
new file mode 100644
index 0000000..c5e3a6b
--- /dev/null
+++ b/app/card_creation.py
@@ -0,0 +1,3079 @@
+import copy
+import logging
+import math
+import re
+
+import pandas as pd
+import pydantic
+
+from .db_engine import model_to_dict
+from decimal import Decimal
+from pydantic import validator
+from typing import Literal, Optional
+
+
+chance_df = pd.DataFrame(
+ {
+ 'd20-1': [0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.25, 0.2, 0.15, 0.1, 0.05],
+ 'd20-2': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1],
+ 'd20-3': [0.15, 0.3, 0.45, 0.6, 0.75, 0.9, 0.75, 0.6, 0.45, 0.3, 0.15],
+ 'd20-4': [0.2, 0.4, 0.6, 0.8, 1, 1.2, 1, 0.8, 0.6, 0.4, 0.2],
+ 'd20-5': [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.25, 1, 0.75, 0.5, 0.25],
+ 'd20-6': [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 1.5, 1.2, 0.9, 0.6, 0.3],
+ 'd20-7': [0.35, 0.7, 1.05, 1.4, 1.75, 2.1, 1.75, 1.4, 1.05, 0.7, 0.35],
+ 'd20-8': [0.4, 0.8, 1.2, 1.6, 2, 2.4, 2, 1.6, 1.2, 0.8, 0.4],
+ 'd20-9': [0.45, 0.9, 1.35, 1.8, 2.25, 2.7, 2.25, 1.8, 1.35, 0.9, 0.45],
+ 'd20-10': [0.5, 1, 1.5, 2, 2.5, 3, 2.5, 2, 1.5, 1, 0.5],
+ 'd20-11': [0.55, 1.1, 1.65, 2.2, 2.75, 3.3, 2.75, 2.2, 1.65, 1.1, 0.55],
+ 'd20-12': [0.6, 1.2, 1.8, 2.4, 3, 3.6, 3, 2.4, 1.8, 1.2, 0.6],
+ 'd20-13': [0.65, 1.3, 1.95, 2.6, 3.25, 3.9, 3.25, 2.6, 1.95, 1.3, 0.65],
+ 'd20-14': [0.7, 1.4, 2.1, 2.8, 3.5, 4.2, 3.5, 2.8, 2.1, 1.4, 0.7],
+ 'd20-15': [0.75, 1.5, 2.25, 3, 3.75, 4.5, 3.75, 3, 2.25, 1.5, 0.75],
+ 'd20-16': [0.8, 1.6, 2.4, 3.2, 4, 4.8, 4, 3.2, 2.4, 1.6, 0.8],
+ 'd20-17': [0.85, 1.7, 2.55, 3.4, 4.25, 5.1, 4.25, 3.4, 2.55, 1.7, 0.85],
+ 'd20-18': [0.9, 1.8, 2.7, 3.6, 4.5, 5.4, 4.5, 3.6, 2.7, 1.8, 0.9],
+ 'd20-19': [0.95, 1.9, 2.85, 3.8, 4.75, 5.7, 4.75, 3.8, 2.85, 1.9, 0.95],
+ 'd20-20': [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
+ },
+ index=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
+)
+encoded_images = {
+ 'Hall of Fame': 'iVBORw0KGgoAAAANSUhEUgAAASwAAABQCAYAAACj6kh7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAASUFJREFUeNrsvWmsJUl2HnYit7u+pZZXS1d313T3zLBnOBwuEmdIk5QEUaJokZJMA7JkWQIhSBBhAYYAC5BlGCAFyDBAWIAhQzAg+4cM0SAgA54xQXGDTIsGOSOS4lBDz1Bk9/TeNbVXvfUueXM5PudE3nczIiMy81WNp9jD9xq336t782ZGRkZ88Z3vLAFw/nP+c/5z/vMB+VHnXXD+0/bzY5/4x1/3a/7TL/+d844//zkHrD+kACPP+K2jL8KDxXsKsYQHy3cVRAfT6SjaujgZTLbHg3ESh5O8KAZlgZMMcTSMwjF9czBJovFkECeDOBiUCIOsKKP9eRYNQxwM4ig4WORBFKhkECmk34CIdEU1PFwWZQiwGiWhQtDvh4EqlnmRZgWWu8MwUyrI6DtlEKh0nuar2Yo+K8tFGARLpN8l4iIKo8V0EM1n6Wo+my/n94/Tk4NZepJlyWwv+aY8DGKIgwS/Y+8H1/eL5wB4DljnP88efIxn9fm7n1HH2Z1AxY+3rmwPL71wabyX5rhHR11JInV5mZUXCCguDqJgdxCHV7OimJQl7qog3IkDtVWUZVCAwjiAIAhCpRQEhGZykVUJMIxDKMuSwQfG9HcUKigxIDRAIJCCnYS+QAD1eJ7D1jCCURTAIi8giUJgzLp7vIJxpEAFAVAbGLL4QkBgBfNVDpfGEeQlA1kBJX9K12KkWWSlfIfwDyL6PUlCeZ+ADhOF5Qk1LgqDoiixpHtY0n0dZjkeqUAdEPgdEKA+pss+ppM8XGTFQyzxETXz4Z39xb37h4sHCV47HARb5R977j+1gQ3PAe4csM5/ngCUfvfxr6r3578Vj8fLK9d2RzeIpbxAwPHiMFLXdobxDaS/ad4TMAWXCSi2Mpq8kyQgtlNEDAp8krwogea+zEJmNwEBD4NGiYqZDtBMB2ZEOR0ThAEk9BoSKM3pSwwqDFiECgJKdP0KkJCORwGV3UEgwEKsCCaDCMqigJi+v8iQrouwvyDwCggA6bpDQsUJnY/bVlBbuG1bg5CAj9pANIwYlQAWX4vPzbDJbeXzjei7dDo4SXNguGRwG1Jb5tR+vidqNqQEgNQn0nt87wTM8t2C/haIpXMTeBWLFaF2EhGRzG7Tee7RrT+kDrsVhsH7y1Vxi5p26/b+/Kt3H2d3ryffPv8jV34Qz8HsHLD+UAPT7zz8ZfXO7NfjC9vq6vULk4/QZP0oAcI3DePo5krP4xGZbXs0aa+RGTYuCwiJ3agZTdgxAwdN/FDpib9NzIZnEplWsFhlcLgQ9iEgxKDEkzUjFhNXwMHMJxfSpCDN9cUYgIhh0eTnyQ0EQKWwKWY3ZJIJO2LAiOm8KwIa/n2SFnBpRAyMjp8TQE2pXQQIQIwHwjAUwDte5jAIGWCYLQExr0Cuz+CW03EDusYhHcMMLhKgEVNSrsXX5XbxF8gclPby/fF76/Pk9D9mgPxdBshIrltKvxARk4HNfUN4B2wGpwzguQbDaUztoX5cUjsJgwWM+XvUE/zvAgnRCLjvj+PgThiqOXG5Gdmxt4jBfoUA9Ssni+Vr796bv/+h8fcsv/Xy9+M5kJ0D1jcEOH3mrZ+M9i6U165fnH7zdBh9G028j9HE/Bgxnpenw2SHzJyYmU1BEyegCUdmjrAhNsGYMJTVus6W2YqOYROLzDuZdNvDUBiSEvAioCFAO14yYLDZBcJCZPIzIFXMgyd8wQBGtGZBk5cwQ9gNTUw5BwMNHxkz80I949ncE11KQAKEyaTEgraSQNjPIYEXg2hAoMDsjI/h+2EQYbAYJTExqgKGiQZXZj58Pn4dLzQz2iK2llZgIqyJmR7dj/QFgaFGHzYlCxgTgHFjlrkYksKeGEgDpc3OGd38lNoGFaNkBsb3v2QWRt9kc3aagLSd25hSf7DZydcHrb8JKB4tM83wqM8ZiBnw+H4e0aJwcRgUBI4Lel7v0rdeI1P6dTJzv3g8T7/4xu3Zux/Z+pPpxy9+L56D2Dlg/YEEp7eOvqh+5/HPxs/vDV68sjP69HQYfzeByTcTQL0ahdFlwo8oqMw0Zj484ZjB8CRRCPI3IZaAC4OMnmiBmGUrQoBUwEVPHgabVaHg8jQilqLNspwAglnPLEWZiLMM5HieoAxCfBxfk8Etp8bwd9LKJGRGxi8GBwYzvjoxO7qmZlfMhPiHAVO0pEyzHDJPBRQzMRuFxgjIMmla0UXyCijFFKXPkiSS8zD+YKkBTUCCAZKYWMrMLtb6FwNFSudg4BsPwlOmxMDEgMgMioGOGSSdTMCLz8Tt4vsSRhlUgAX6WoCFDPZ9auCQrscLhaLOJ4yvgF/RvRTS/6K15ZleHIR5ofRjrEo4SEu4MNSMlNvDbJTby/1D/VVS247p/a/QM/nS8TL7dyeL9Ddfu338pQ+P//Ti1QvffQ5i54D19Qeof33rp1SydevG3vboO8fDhMHp28ZJ9K008S/HrCIpnr/aBKOZI+yHJ8WqYj6xCNhKTBQlrCMU04knPE9mNk34JBGBmKoUFAY7nmQ8ibIygD2ya5KQJ2Ugk4W1ozTTwjZPOgEfZCaiQUzIg4g8obAIBgM+d8hQitVQUBqwpK05ivk3iJSwlwU1PhOGBnJdvgdmeaW0jwCGWVSlcy0IfLgNjHUMLLEAgmY6DCrjWMExTXy+J74e4ylravydhNrKoM6sJqf+Yf0sF00KBJjZnGSQYJBiEMvzXDNTvhcBjvIUsAL2T9KNszNgFGpWd0RtS4JSVpASNqDDZujaHOXzsAnK/VASIBZkl3NbJ8TcDpYFgbg2ubnd3A+haIGoVyW60UmsBOCY7TGhpGvM6Bn9+zwvvpCuVr9172j5m2/dmb32Izd/PDsHsHPA+lqbduozb//D6CPPjV66vD3+PgKM7ymx+DS9/RFiDzFbUdoYAfGQMVti0NBjV4l5xq8w0GbXQAArEI8bs6CyWuHTQglwMCNBxQJ4IEDBkMYmGM0bYUqDOKLJGJKpFgoDo7WezhvAw+OsmtwadArU5+NrxkGw+TuKKtMTRAsLAq0b8XVZB1qxaYYgv4d0rSmZaww6R4tCAIAnNH/GbZ5U7EeupbRAziCTsklYlAKQokWBBr9IAJbMzAEIG9ShDoH0E4MR60jM9ri5sbC1UsBR+ri6NoMeA43oZYDC5pYV6PBnWV5ocMMKQIABNCewAXEiZMJiSzmfACUdz+BWlkX11EuZFcy2+PsZv48a4LhjTgi4URgyM01tPjKgMSPcJnN1Rs+KAZnbyo4BPp9Sun+KPBc97YBujO79Pi1On6P2/yq17/Nv3Dn68p9//icW53rYOWCdGaR+9c6/UKOtd557+er2H6fB9gNkIv1JYlA32PgpxXRCYU3MjNjE4kmmxLQJCRAC+V3IuA+0/lRqcGABXFz8NMF5OgtggAa4Qlz8WqPi7/FxeaE1qhW7+lEDCpuEYRjBziiUfweoweloWQpY8vHMeNjbxmwk5+8FNFvVQAvSxLBQJfSKBZhKuX5Mr4jOG4soL2yD70hF8v76882QweovUZE0c+s9xLBxHglp4PCGMqdr56AEkkoBfWYvWPJ7qYCKqsCv4GOLBd1/Lu3FckWfpwJWbMbm4g1FzWJVKWESDFLcBwxgbA7y+0UFgkGAldhfgjYCS63dQSEaHEtnS2JdaSUsUgvEw8nAxbeyyHJhnMx2h5G+z+M0k3MzY9OLREnPMoNZrkM4BrJwlXTukAW0A2Jxv0XP7RduPTz6hVv34I0ffvHvFufgdQ5YDYD6zFv/bfgtL42/fXc6+o/HcfAdxDZeGCXRR9McoyjUTCUtNFNhRpCL/oIyocQjRwDCf0c0qZmVoICRNvsYIBYrFPbF2tOIGZJ0OzOsUB5BoFi/0uC0/mySEBMSwKPPtcNMAC0iIFuRSbg1mdK1ibVEQ8gwhpOUZbABTbMYSsWgNqRJmOjVvQ4pSnkev6p4ojoV2UGAALX6j2twgdO/4fQ4NxxtQEmdXkpV4QewDkOobEVlv6cqt6XdUjw9owGCiJu/GfjyfC6gwi8sF+z/o37M6LMlgRexsnRO7E2HYpTyHEthbQxcoXge80prZBArRGebpSthjyzHMQPjwAn+mxkhAxZrYwxOKf8d69CRBR238RsUNAYK8agyMxSNjM49SMJT87PUsXAlXec9YtZvpXn2u/M0+1dv3d3/lbfvPTo+B6w/pCD1c7f/weSVa9vfN0riP0sD5we3htHLZC6Fmv5r045ZCgMQmxICWMRoGHiYDc35jQpcVKU5DaJYQgRieoVBCDoKiNmAPgaFOUUVA1PCoGI2+1CDCpuFSTKluRpr8waJwcGAgGhIIMQsKYE4nmgWJV6wzeMT7YtXbjZD6gDCjKHgFzEY/pwpHwvo/F71bwEl1L8128E1DNiY1jqKNBYq4/1NG1Xjvfpnag1qWPu08oIqRhClHQzyHr/YnmMzkNkt9af+rHIJrq/GfRRujjUgtfJUMrAxiCliZ4ApK4IEbGl1GmKo2RE9r0IY74pMOjLnYGcc0PPP6PSFsDhmWstch3Hw3zNiYlsJsV46RoBMdDmOP8sJsHKJKWO2yIshv8ehHkVlwmpNUAfTrqowjVg0PXwQqfIXD+bpv/y99x/9qzfuPtw/B6xvcJD67Ds/kXzyQzvfPx4O/hqNtj81ScK9LEcZOGsgYi2HTY4pLZ37y0IYETOr+QrEBOPjaByKKM3AJlqUgFRAnyXCqPiVBFqnYrNKTENkj5o+vyITq4AhFMGY/h4B0iuMp7Q6bxjR6eNhLKHVuGQ3PwvTmf67oElRrtgsoldeVJHi1X+qmpQKTTZSAxSDaNXew9pkhwb/wqp99Q+a7O2UVykPeCllNUe1AJ7NDJVFEq1zYQV+9d+4Bj4yoYkNqwrkAnqWikP9Y/1bMWKcWqoVaLPoXqygyE8I1OYEMEsYBHNI0yPCwJV2QMRAJvlKTEHW5/YXKwmdYOBi6GHmRsOIxk0m7E/eFzYnaqOwsUIWCf0MC1pcRILkkI1Sa2qs422JmF8iMbQTMlM/l2XZZ+8czH7mt9+6c+8csL5Bfi5Nt+KPPX/hU5emw/+EVrK/sDWMb7IwyiJvHGgPGI/5rUEsQZDcJcyqBjSw2ZO1PUyEEZ2kWv9JiCHR2BQ9ahRFYpYR4og5mMSJmHnMpBiw2HzIYUTsaEy/iSUFE4iTbZo0cTXx1KnOK+CzoIFMaCiAlPLfKwEmnb6ibS9sgFDN7DoFCTQYjWkSuv4N1vE1k66BT8o5ipR1vcbxqg594AUuH5DaKNsFnMZtYc3crN9XBWb6d8WUma2xiU/PNqBxEJC5ppJAA5p2/1ZAxqbnihjYMYRAQEaAlgQzmERz0bGmAxBwWuS5mIFJzKx8KeZlRosMv1DMVY5RE+GeCG6hJBqNdbkKLNlJEKq15gVyvrJyZFS5m0fE9n7ucJ7+9P/77sNfvntwuDgHrA/Yz5976b9QD+P/85Ur26O/mUTBX6TbfImjxdnMi2ngznLUJhpqM09JIGYs3rwo1IwoDDQYJSF742JI80DrTsyOCvZ+0ftRpCN9MJTrFmSyCTgpMtvibYjiLTERT7uZwwKWmQYmWoVzegkopSsxETQY6VXWy4SUyXrAwWbcoFX7Tsu/19dQ4AYyE2vMa/rYHDjb5wDQNgaoPMzPx+JcwOUwS+3j0LgEvc/mfGXSq0CDWMhgRnRKEYIEcWWGVkDGILVaHUKIMxoZJ2TdHdObcxgPiZmvUvE6ssOATUHiXqdmZUEgtco5mj/XupmY6IXoWvy59jZqD2eaa6DjIGGO9OfczJi+lGb5HWrkZx+fLP63f/fGwW8+nj8qzwHrD7DJ9zPv/YPkW27u/sAwiX50Okx+KAzUCNdCMTA4RcKqNJPSQZkaoBRcHA/FL7WVEHAxY2IdicBJQSimX1aEAlbMsoAAar6i4aaGdNId+j2mgbstJt06lon1oZLsyGLOwEQDdUar61J7utZeqOYTUMZk8QKExVbsSeYDlSYT8oGWNdmdYOkGJpeJ6AM4aGV9NnC1A63yXUvZx7f3h48pGm0SZGIgY2YWChsLaVELaOwEI2ZllWZWmZbapDwAVRwRgB3SeDwRj2USsqC/EjBi8MroOAasQLyXuWhZzMJozSRzciWAxbUvFplm3Rw6wqElM/ZGsv6FpYStlPSzyla/cXt//j++c+/oZ//D6z8x+0bxNn7gAev5i9uDj9+4+Od2J4O/QgTmO4hF3eT3RT9HJQF/rE3pVJVAtCaO4GFtSf+b9acIdgiwWGMaVmadgpjALKFzcGJuIKEJKY6gCKaQqSmEiQYo0XbYkcasaZZCfpLC6mSuwYlXyVPG5DC3nE9BNeaKj1k1WYzPnHIdY+tWDjB0jQ7Hm81zKre56QATJ4DankzlNlGboGWf2wdcqtOk9QKywxyt8WECpVDMS9bKQh5LgwrEBtpVKJHzBErp6gCAQAyzh7Q4zrUoj5kAFDOwVZER+KBoXCW9z4BVoA6v4L+1VzOHUaK9kayJiQYmt0dgSAC2T+Nxkeb3ybr4Qlnkn3nz/uG/eP3u/vE5YD2DnxcvbU0++eLef7Q1Sv7edBB/coU6wJIZFP8eDwYicPPfHKc0TTRbYs0qp1VxlCQCRHnBCcWsU8USXpBEA/ldAJmEOCX2RFQeplBGuxImwOhULrUAns0JoA7nkJ0saRXNQSeIoHdiN60s5X0STtbjYBVV+psH7Do0ocbk7nONHkDkZDb+6xsmrM1klK13ObS2FlBWXo1OOZmcCd7twOXuXyuMg/WxkuPl6EVjLhxG8lu8l6KLcQR/Soz8MR13SBx/SbhzTABEYwvZXCzES3m8XAjz4rALxQBGgMY6GHsmOd6LgU2CXFkDk8BbJMBaCgtjkNzlrATEx4eL1T967c7jf/LmvQ8mcH3gAOsTz1++9PKVnb9BjOk/350kNwk7lLinUQdpSrYHZ+TToCirtBWm7aOYGVMs5hyXloujhAZAJEGVA3qfmdU8p+ODXcjDHVoddySinM/FAJXtzyA7WhA4zXXEciWD+9iHTxRvYyaup9EUuVUHI3NrUKoVJJUFougGXFv7cZlKPnPMNv9cpq/PRGszm5Vbt1NdpnYbk3KyM18sm3mNBrg3+lO3MSgYxGj8JUMIRzQWxxEoDtiq4siy9CHkq4dQFg9hGLKUQGafmJDsYWTGlYr+xaxqvlrJ36uykHgzqZFBYLc/X0kkP8eMXRrpsI9EJ9A/zPP8n956fPw//crvffX2OWD9//Dz6VeuX79xYfx3hlH0t8IwvFBUOV8cLY6o46E4ninNtQt7EscSx4SlSOhEpRMx80KJ2E4IqGLx7IX0fqq2IY8uQMAgxRoV2Zb5cQrZwRxWh7PKvCscorIXZZzkyM1Kuk0up3lmhRC4wM02rUxAcYcodLE8p7iONbBpYzQdzMvP1Nyg7PJmKq8A3w5cnZqfC5wdgLQO/3A/M7Ov1v3IIBcUNBZp8YzIMggnxPLH4bq4FyxTMh3z+wRCbD4utdaVLQnAeEzmwqz4PYn+53FKDIw9jfvzpeQzcnQ9V8QY0no9HUaS0jWKeXHHx3eO0p+68/j4f/i112+/ew5YX4Ofj167tPXi3vZfv7Y9/K8IR54rueJBFBEgEXkuxBEtEcMcRR4TCC1zJV47BqycAIrZFVNyjpEaxENhU/Ocjg+2CKR2aYDs6kBMYmb54QLSB8ewOtqwKJfGdDqYa2q3kaKiPKI5eoCuxSRrZVq1w13xU85/20DRFcbgaU8rKHjEc4QeWpFxPx0g57SalUWWVAeb7PZqOkMyOvu26/k1+2y98ARiQsYQD0aafU0iMSHZYbMi5lVk9wDpNYq5PlkJJ6uUTMSV6F6ByiXcIqLfd2ixXRJYsfeRta9xQqbhOCagU7A74hI6OX0uZXUWZFb+ky+9//gffuXOw+NzwHqCn+d2t+OPv3Dhr1wYD388icOXR9TJ+/NcJweT+bdFT2A0GMIRl1SJIvH6cYR4WerfW/TZquCUmUi0qpDeK6IxARWxqOFFEcyRk3T3iUU9mhFIzUSHwqah53b9gxloqU0B5WVVyiOwt5Irp8DdwYoaLMmtS3VpY65WIvju0R3X5deX+nnmVBvIeHUxP9i4QLyNRbV5NxtGvlKuJ+qeYS4G7gsfYQG/IPBKRhBNmX3FOpCUASq9DzHehyx7TNYC+xlzKKr0I4UruEuAdbzUjGxCYMVBrVsEVAta9C+NAzhcFhJ5L/fDlSzS4haW5U9+6b37/+zNe/uzc8Dq8fMdH7qy9+Llnb83GUQ/Qkb3y5z0L+VRJOG4rGqNBxIbNSL7Py24NpP26HHogZh99JoOR2TvczAowJJMPhgQSMUTXbXgZAXp/SNI908qJoXNwYzYFHotWu8Wrs/i/XOL6qrVTHMxOIspGSHr4A4zaJzbof301bGcE9+lKfXx4Ln6td28BPAvFs02+YBTOfCtzVRVnsfsg6I2puoIIXGsFmwtRJhAPBxDOCXmNdLxfxy0iqv36fd9yY8siiWwq+je8RyOFgsR9eOAa+8jDBIyJsg0uDDi1CGuMpFLtQkuI8QVWDlW8SRdvV8U5Wffe3j4k7/5xu3b54Dl+Em4qNRL13/o+d3xf59E0atcAYGjfPlRjuMIOIUmk6JykbCo3fGI8CyRAM4tsvtVMKAHxaVT2NtHoJVswUE+IqC6JJ6aYpZBdryEFTFeDtgsoXCth9bK6dBGvAOvS2j2T7ZOvcoa1KesTqn2Vd4HUBUYr9/Dtu/0ZXotwNPuuXQwqNaYsA422BU/5dTNOnSqLvDyArnykCePB9j7cbOvgzIiq2EA0YReoyEEg0DCH1Yp4Uv+kJjXPtw/OoGj+ZzGekbHprSw57AzUZAMyLwk83GeccR9JtoGx3dxZSJO7OeQiVCHEx4slul//fNffPt/ni1X5TlgVT+feuXay3vbk38UheFfSMIgkF1TAqkoJbl8zKaWtBpQ/1JHRsSqQrg4mdD7QzqGwEklMCTKfLAEmMEugdRlMvkGku6yunsM6UNiUhxs5zD3XCHOzrQOh4mIHSaAaqP6zuPVhu0p1SnIuyeO8lieqoWhOVZ5b7xVHzPVJ4arzpCLutevEbJxhjSifqar6gBgX39j63e9epV3LKhOuaBOnOvfkXKPtGhHagiDyRTCLaJQEbEoMgOPjt6C/YM3JTg1CVNIghXsTpDMSxCv45LASpuEhXgepUJtqUtPF1IzTCq94sOT9JfuH5z8l59//dbvPWusCJ/lxZ+/tDX8ro/e+LvXdqc/PYqib+XkA6l/JNUPoiqpOKJODcWrN4oGEIcj6nx6RWOIowlMB1OY5wPYx0uQDp6HINmGclHC8v19mL3zCFZEi4syr10VW7x7PnHVj+2delDX+y3xWn2EWugKD/DpOK77Uu3Cclu7Ot9TLaZoWxxYl9De5dlUZxDXveDt0sFcGqQ6A2tWxqKova/olB+wjdHy55zao5hdzcS7TdYghDRXxltXYGv7JlcQg9VqIfmtnL3BG4Vw2SIpBqnUaZpaXhVy5B8dHYRSB7dE/PDuePCjL1zeYYbwbx+fLLI/dAzrBz9585OTYfK/5qi+Panqb5+kXGGSGFWSCGBtDViX4hCFASB7+gI2+RLRropyADmZfieKAIoYFZuB+cEC0jtHEsgpe+o56LXhfXLFRynn2tcBXMpN41W3xtGmb2iWga1Mqp012St4w//WyrawAzRVl1mkOsxrbzKzywPakfPYVimiw1xuZ4rKI1m2RfP7z71mjRv26Nc8baLfZLnu+D4GvrCMCZAmkOxOJUSiIDZ1dPgWBPl7MB1yBdaVCPQn6RJWxMDYdASl636lHATNjEuhhExEIvIXIkGssuzfPzxe/Niv/v6tX0PEb3yGtTVKAgKrv3ZtZ/JTURh9eCUF0aKqtEss4QcJmXrThCPOB7wlAmwNieoS5R0nU4jCCXXsCPbVLiwGz4GKtqE4WMH8jQewvHcEeZpBvSOVOgtKK6+orjzmoT3BXQzKpY0pr4yhOiwT1bHquyfBWSeY15ulupmmbcYh9DERPUxEebx7PrPyiapN9DG1rQqrLdUnzqJ51gHWW71VqW7aUQNArW8S2OASstkCYI4QMePa3oNk9CJZLAHsJlzxcSApaoM4lsKUPA85ZIhzZ3V9fKhq+IcSJs3Vbum1N4rDv/qhvZ3szsHs36zy4uuKWl83hsUP5Pu/+YXvvTAd/Xc7w/h74lAFs7SQXVakdrls0hlKgvJkOKbO4+TjIYzCBIbxGLIsFEB7kE+gHF3WNaUOaHW4fUyMKnXrU+456FmX2nQG5XJ8dbOerkGMXeO5xi/siX8WU6+HadSm+zSUmx7sRLUBkcF2/JH3yvPQ2tN97Gs4nmftfKpjZet0nriO9S08PdgUtC50XbPWrSXKxhrIJZOmMGDGNeGilDPA1VswCh4Qi6IFP53TtVJI8xUdzwnZHCW/on9nRBJQanlxFYn1hhy8IUqa57+xP0v//r/+3Xd/5euFWl8XhnV5exR/36sv/PjVnfH/Egfhy4nsQaVk8wR+cboMi+mARGNpJRglIwKnEXXQiDqZwIrMvzlO4CC8BBjtQnFInfn2PqR3j6VeVLNOuGoFrFagaBmg2AAt1XIRt4mougDOeRtND1Vzpe9gg6oDIMAR3NmpgXUAtOOmTF3LFbKhOoHuFCARG0Ckuh6nUmc2223W6O1PXxs8FTLQZdx16JZOM9Hb2PXTVMK4ckxhRaYfzkuIkgmEw6uwLHYlILXMeR5pRxeTBqkRpnSlV9n4o9S19Hk3oNOdupV6fjJM/rObVy4Mlqv8144WafGBZ1h/9JXrN567uPXPtpLoT40HsWK36TDUdaY4Up1TaRhzSt5wARIYxyOpRz5MiGXR7yWZgkfqIgTREIrHtAK8dwhFVjgMMPvxm7b+eoDUnyeib3y0Rzm7LcMecUDeAQ2etA9wV23o0D/MOlndx7Xm0XlAqxW4WqpIrFN7Wh0SnrrzSnWJ8k1w6Bc469PYfG3oyU4RPDFW6DSH/euV6pi97oVaee5RFjuu50aMa3hhmxhXSBbMCaTz34NB8IgwKiMWtYC0WMIs5aRrLvWcyvZoRymxL3ZiYa533OZKOjFvaFL+37cfn/z1z732/nsfWIb1R16+9vyV3elntwbh94W8J1YQVDWodL1zLpDHMVVFzqJgArHSzCoOyOwjc/AovAhpTKwqRVi9fQirOydaTMe+tFi1DDhojeXrDACtA0Jnyk1fMGsOPuVDMFcKi3VuNwn0V2xwt79H9H6toaqFvflYRx+vW7c31m3K9ymA2F0LrEUicIKX38w7fcLo6VuPTro+JWKH7nhKlbF9gAvjWsJqNgO1IAIxnEAyfo4IBM3FjKun6i3oMtlCLTj1ZPIWb5JtQh9ynqLsEhWFnB730iiJ/xJZUZ9/58HhrQ8cYP3xj9/8Y3s7k19MovBV3nq9QL3tVV7q3WK4SgIilwrmROQh/U3mXzSlf4+hiCewHF2nv4eQfZWQ/91jqdTZFpHQJMpOSDDGbbuvxadnnQFUXJ4y50Lbw1PpW01btB5j0nqi8wE6KhP4JqbDROkT/d1WoaIrrchZ6qZxXY9mBW0eSz9w+fGqw9nRJfS3OUZ6OGLaWC762JVjMRFxvliIZz0sExju7IEKrxCBWNB85Hryek8DpfQ+mbJLVIW4vHPUMIml8ik/l1EcbhF4/fDVnekX3nlw8M4HArCYSX3vqy/+6Et72z9NNu+lUIJAdZ1zLifMYQrbg7EEfSqO1A3GxKzGhNj0ghEsOBl5fAmKgwzSNw8hP0wBsCvgsw28OvQseBKW5TBJWtvhX43bXOet7NCpp7RoMG2hE9Aet+XWo1oYiXdCd4vkpj51huDMTj1sA3i+woFdfeUPyrWeYSuAdTBOH7P1i2rupboWRlG/HXQvt5KHmKYnoGbEtkZTiMY3yAyMIcA5DCRuS28GjNV44TpzvKsUFx7gMuOsc3HaXFbglH7/5ecvTO++9+joi+XXOPThawpYH3/+8uXv/vD1f35xOvz71OED3mGGN7KUTUJLvTGnIlYVqYTQmMw/YlBhQL+JXe3nMRRbVyBKxpDdnkN2a0ZmMrYTKeUYPQal7pboXDWMGt44j0rvrCrgc+v4qo22ibEOVqV6gaKfNXTX1/LUoAJ/KWYnS1D929Y3BUZ9DZLB3aZiH23M9Nq480vPokOp7oUAeph/PRZV96B39/+abUXEtgZblwCiK5CteMegTDMy2RW8kHk9l0KZNMdL7QaSApZShklFg1D98MtXd7/1wjj55Vv7J/M/cID1LS/uvfjylQu/NB4kf4Koo+L94qSCAujqneM4gcmAo9QHMEnIXg7HBFZj2VXmYTkEmFDnzFirOobyYAWtUQqqhWmp/k9OKfdYUj6ENHMjnoBxudhWw78O3VqrOgMzUB2eMo8J22uC+Zie6slAzPPanrJWj6rqwUhag0W7QasTvFwApmwao9oXuDbngu/pqx5+cXUWf5p5LOfZLpbHgDQfk8EU4vE1KSpI0CT6M4MWx2cxn2BWJVZUqKrNflFMRDYih3H0sUvT4Z8dD5NfuPX4+OAPDGC9dGX3Atmt//s4iT4l8zrgCgphtaOMkhpU2wMW1BNJq5kOtuT3fhrBfbVLxwwhf3cO+Z0lYIb9FgvlAa7miGm37B0X6tQAWgGrz6DxlEvBFsalmk4E5bi1rvInncDVEXjZmMC+EA/VPjFVH5OrJVWoz0YXXo1L+RmXL/m9F+OpF0fsHQCqWseaP9L/yViWHfAAtgOp1tQMU1geH0NIpuHo4gtQBhchXe7Lbto5MSwO+h7xno58fKBOnQmhWlsuCNMkvDJI4h+5eXnrl75y9+DhMwesV65euPz8pe2fnyTR97AJuN78IVah7IScSGXPREIWRvGY7oKAi5jV4zyER2oH8JiQ+u0ZlPPSwCHsC1r+YBTP7zbTCtwR6D11BPeK15KZ35Kq4pzkDjBrmsUtk6pnSZRGVdGuCafOxrbaQcsxjZXtBOgTra5azeN+LMd6htbCoNOm2h0mnsi29jZBuyPiNPUI268JXWt4LzOTg0ZnoOYBmYg7kIxvwHxxQhRrIUI8l6Vh1sUMi+sV5LJzeLVvY7VdVV4Uu9vjwaemw+T/eP/R8eKZAdZLeztXr+9Of34yiD4lm4/GOkmZBfZVzgFoHFc1pBvmJMsx7Iy26f5HcIcQ+zjYhnK/gOK9Jd2RapAl5WM8qsU0dHnIGmdRneZh9zNVvei3SfpUz+J9fSpd+uSTnmVgXCI39DAln2Tyd3pBwVMpAv1bkrWK4GcV5/suKO0sxht028Vyzyq4e+ou1+MM+1mEqn3uWOCcFidQnOSQxBOYXHhB9vDE/FgE90EUiFmYS6WHzU8Aei9rFuRHg/jGIEn+NJ3tZx8czU6eCWC9ev3iXyWg+lvc2IHkGemNRQcxlyPWlRXCYEi/J8SupvL3O6sIVsEEitsrKO9meiJ3dOkThly1zRCvB3vNsPxBpa6B3QFvSnV6Ms+kpzg2iVCtYNgHtKwYH0csj2qd1N2MBVuA1xf7pKCf0N7JBlv2WXSnHTl2wOnxfPqJ5ea5VQ/ZQbWtzn1TdVqsC2xheev3c1xBNl9AXA5gcvE6Td0hBMWRVH0oQDPOgHdTpxfvSs2FN/l0nJfIqTxJklwnqHjtK3ce//aTYk70NIDFyMrh+nyzwyrWKpDJzpHsMQzCAQyjsQSDzrMQbqV0Y5hA+Q6xwqPmqmAbbtCXwbq+gGAFYroPP/03Nm343hftQjf0bf2FjgGzbjpuwNzwYKL+p3XO0ywVxy49aB2rHDeNCM3P1i1D/Qn6SAbqZNv1Ho0bsre5K4196zuroMuQGLG5mUZ1XrDKTzcqWKy/a7h6rbpi1TGqLvw57mvTJ/b964KJm/tHaKRONB47GixuY0LWz13PwEBz3KL53MFKRzqtnoY+pwM6xyyiH+X0dfzUIYcMDo/uwnR1CXau3oQZx5SuXqe5HsjeiiV9fbEqZdsx8SaWetdqDjDN8pwZWfA0mPNUgFUiqgJ1p3O7CtS1rBI+rUoY0rgEH8zzCL6KA4BlCHgr5ZrFUJ+PPiDxhzZVDw57MK7uaNMGrvRcQB2I59CcGhdQnhm/mTyqAYjKAi0AV5igxgSsDVblbCPWxXwDqarJg64igps2GmCEm8mF1QTR53XcJ1aAq8AELRuUq3bWP7f1olMArKe/mGjp+Z69eODpvMZ6zpaLNaLV1wqafYgNG1GDQAUqiDbTqS1aWI+ERxOfDJxb9/OmfXbyv7IAzznTfKwf25f3UpVwtLgPkzs5TK6+CHMiJpj+PgSYwzhGGUUMWqxhlfQea9uy0YviindKPTPAmmeISazpHk8qdmby1u+8pdYoGtLvERyuFNwjVgX79IU7GbU5AJ+BgY7f6Jv4LuLkTS/0Fn/xO3rOyvAcrhY3nmKL1oFGwTZ0rea1aqebRbTGlIzxiM3aUKeTEz03WpuUdeCqf37aHGvSoWMCSRKtyR/x9Lg6hVC21tswfTftAfN6zJ7qHkULuNAFwLX3NrdmsdYaKJvs1VzgsA5Qp+3HxphAazlGNB0XdZCqAxia/6u1GR0DTW2er2fwnla2RUNk2AB4l6xSNfAkfQzFVzOYXrkCxdYOHB38ukRjcfXSSEWAQS4Ehs/JJiLHY4ZPh1fwVPSMO46LewVBIMFjoyQWsOIdanhTiMcpwj0YAhzSsXewBlYWi17T9Q7zsBWcWiEILZELvWdG24o7a6cgQuvS1pO5oXd9Q3fkP6JpxhnkB2vwXzsErU8QawzAZDzu8j1oMVM0TCjbpDL7HU+bjOvJjKaJjPX7qvXr5lbRYe7otq49VcY5EcEuOrc+xnjfuGa9u6rj0KMp1D5D5z34nzbi5otGfzrGU73NWD0zrJl0WP9e/V5ww3Lrz8QcBbVz1t5G43qb/lrkR3B09x4EywR2LvwHRGJ2icAkIgmxAy6QaIFQsKL8GgS9P509GRDRC0NiUyFMeCt4pUX3EkPYXyDcxzEgh4vdRpAK96albJh4Idm4vPfamYHLR2XQ1lHQw7TQqz2gAxt69TmqBng4zU70gI/xGbpbiWhMbPS10JpAblqKxjNxg0HLhHedE+tW42YSNMDMBjg0Jxm6ANs4tD4xDZQwgMYEWGyAWf39JnhB4342wOHTNTfMrN7DWL83B/ihDRq1xQU87V7fP9qA6WwbmtQQnYOoYlrEjyIFCJ5FuDpPWs7h4P4dULMQprvfBWU5IAyIiMzoTY31btbaWxg+ZX2YpzIJE0l6RCkQVqKm8VGo469UEQM8ppu+x6arOxDk+scj2H6pgN0bCra2BzCdDKWg/p3XM3jzNxZw/52VM+zTZ6Y5rBrfET6F3knZEBuMvw/99NI/Q/doFeVrK+Kp1eQN+ACsWVkIaG6asDYvaiCu6maVYaJttBZngKmjvc2YJNNMVGpjDDefT8UsXHFiVeejIbvYJt+p8GMI9qdtMLSgzTPH6gPXPZpmpMVMq6Th035uhKw0dSkwfQSbv9fPAl1hEWhJSnUT0WWu29qidX1jeGJDMuF/bl0uYPLiMQwvLogxRTAZD2E4SODgTgT3Xo/g9mvK+fy51tbBg7uwdekixNGA7mdAVlcGIWZSdlmQiv49TqKn4llPHNbwY5/4x7CPn/ujSRT+8ChOpAIDx11xkCiXNi5zYlnvg7hAbdoz2g3gh/72FXjl0wO48vwQXnnpKjx39YJ8djyfwwwPYecVArLrMRy8h8Abe6geFqDq/aZ9QP/YLPVEoHWGZrVueNFWYE8178gXKtGIK+xZadTlaXTcZ9su1Q23RGfeJTi+25JobKUCmbejvDsi+e/HF3rSvgMOdnqarXQv3046ZxxXdU3MH1jq+EZYwrf9gIJP/IkCti8jvPLyHrx4Yw/iOILZYgmHi0dQTB/BxZs5pAcERsvA1MPEF5jDar4AFd8nLOCKpVy4gEArKE+libIsf+7TW//Nb33h/i9+/RlWGOhGFyW7LbWNWlZlZDjydR3pWu+s4Y6CP/O3L8DORUWdMYTdnS2IiJFdu3oJJltTWGUFvPb6O/A7X/4KwHMpvPrnI/j9nyEET3sCs2ojT10hv3Y4gJkc3S82y0XNml/0Ns/jcTREXoWmH7IuUIsAvXnfTDmxxPuGga5qVnST1dWFXqMFVrxWU6x3ifKVUwBd3khV094UWM60U4cB1pijUmh4uLDGwrAm4p+K82h7TlUzpMMKQcCal7QpeCvLW6caJplysTC0vK0u0cSqJ4+IHTFhm7aqht6LDVWE2/VtP7SCG6/onapuvnCF5nMMly9dgI99bFeu+t57d+C3f+c1ePDoEK5+egV3/s0VWB5GXLjP8oiWUBT0CnVlUg4w5UgCPozL0RS8h8OzMgm5kBdHuHMBr6gqPcG5RbzZ6SoLncbcp//iFAZjhHSVwZXLu/Dccxchiskc3NqFwdZF2IoS+PSV5+H6tT14++334Y13bsP+d53Arf8ncq/4/dVqd4xNn6pYjvjQ/vFh7ogy5WiqavUuqFbj1ox2RdNcci3K6EjfsLTFtWvciM3B+lbEClp7BNGxS9GGfmDdnDTdfxtrTq3DHOoAWe8sNE3YOgtF06zaxH7V4pfU5ji0PcuIDia49pI2+04Z4IX+oQB14IPGYtC0IWuyAGwAFmpe5I2TyNKK7cwPh/x18ztTuPgcJzyXsHdpF25cuwiDIZmC4ymMd/YgJGLxib0X4Oq1K/Daa2/A+7fuw+wTt2H5+Ru8x1h9JZVWMmlZ702hwxk4fqC0PKPPALC4b5hdcRXCWGgfSs1nDhpbZgGsa0SvH8cFMvPGezk8OkxhOh7B9tYYJiMyI5MhxCza838BAeBoBDc/8qp8fnh8Aref34fhpQDSR0Fj6vaKsnJ5rZQdOOGefL5VUp2lkxyTua6kIPTOwG6IdeZErCOUCT3otD5qvdfVkdahqgZIm5gpOyzAZFxGtLnFOg1dZs0mqnw9W+dSjQdvB6g6+qau66FHe0N79djwyQ1mW3FoNRBH5Qj/8w4BtPwNytLa7FFSAyPLKYJ1dmsrXuhwdtUD7ZMMLn74CPYPQfKAP/TCVZqbNCcHA0gGQ91fREa4bPmVF16G8WggX3/z3bswvHEEi/d2a49Rz6eY9SoOc1IbCseVgjnIXBVPt4H0U4Y1BGo6jKWEKoc2hISkBdds592ao6Dhyr74YQKy5QoOjxZi+jHYLdNcyq1iWQAWKZRk9yK9OLx/e/cCXLtyiW4cYefDpq/CBzWtMpXTe+jytbUzrTN5DlXT62cOH2U0rYOu2Y4h81ytbtSNFwnR7yms+8hd4Qw+jxEaXsa6J8p0LhiONdvlXpOVle/W12EL65YYXi4zNEA5vJiIzXsw3PWGlw9dvejwlmLNU1e/Jtaei9d1VzPjNt/ZdE1lNqN5jMtLaPel4Zl09iXA8OqhzL3D4znMFyt5AryXBDJN4tpWRQaYrej3kj4pYTTZhueu70ne8PD6oenRrK4tQFUxaHHKce13wgiuXPq0u0g8FWAlkUKOGZXdY6mV/DejKG8DVGCT3oY7M5jNUziZLSAjwGLgYsDiXTvybAkFvcpsAcVqLr+50P31qxfh6t4uDC8WxvxXZwEMhJYoVJ8A1hJp0JMIubQJ1/U8Q9DRDNUJZHZIlJNhQntEhR1qgKZPv6mx1ADDWFDQE7vlCI8wwhDAil/yeOfdrnbzHk3MRAvsHT2PVmSSHZOF4AA6V/9i7fpo9kdtcjufE9o9gQbWIfqubQMYmIsP2oBHC8PkgICK5+QSVqscjuk3z1E9J1OaizQnc5qbq4W8sFzBzvYYbhITG0xzgLBoDLKikJCnSr/S78WhBqvgWUa6o9JJzyUGIqhNB0oayGAl0e+1m+B4jqzI4ehkIX3J+5ydzJcwp9dwQIysEuYiulsVajEvTxcMivDRl69TJ9yBd38+PRWUVQt4qTb601lmo7sOx9m8hmjSuw6Ea3A8JyO09Q2waLnqcTtouL2V8rQbTZHcNIGwqZvYUeaGCx6aBqlhkrlNyk3oATjuDaHRCjMG4lSPOr0HZYrijYwA6/sIVsoOqgZbN0RuZeYKNqMNsB5YUZPkVEtMM3q8yPXcQLQKSLqjq+txdmGs4NHBTN7LaO4xmWDAmtC8HHPzuYgogVcQxnI8EwukefzSi3sEdEs42CGr6VFYkwuw5pQjTlbUg4TLSuF6RoDFelVaaMonTeFGcsXB9YxGaxVWmn1xMXve+po/599MRfnQKKPPwpTs5VC+sCTkn80XcuMXdnjH51VrRo6vlhZCI6vCQcHsyd4sdWZHwXe7rl0tRIcS3hTkO9wAbkFXoV908p6wDly+WKpqWisHmEI9DcTyMnZVf0Vs6ki2GdlIK3LUs7b1GVvX20Q71TC1CUho5VWacV+mB2+DScrVIbXxgU7GXf/6Ka5Z6RWu2utG3zpyO42UGyPUypMoxvuD8pwkc60sSg0q9FrSnORnEtP8DIhcBEGoi/qtVjCbzQm0StjZGglzWhpJ2NokxJrUEUtaTil14JdZifGzAyyp4qxrQefUGKKUg4iAhrhgaJQUVtIZZRbRjdPN0Yu1rOVyKdSTRfoFoXXOdjJ1XEz2MT+8FXXO8ckcDg5n8PirWYMw2CDVauh5s6q71FHlNRLP7jVED0Cqbr9ga5sdAOJsuWpJacJmSELDzFC1ANDaRLJBY/2JBaKI5pNSNY+m97poesWU7VVENzCaeYCb3EU0EsWbon3DrKyxPwPKHcnTm/fRan6zbcrYt0vZ0aRNC9EK9FToMt9NScMgd9iM9+H/F6tIcx6eb1lO8zCFPM8JdEpYpamwLdlcNY4qwMqERBydaM3r5FFieHElsAF1YDSDNRMS2Smac46jAGZLeHYmIW+GKtHuxJhWpQ7DZw/BMAkJbGpJrlUHLu8PYfqixmO2lV97645mU7grnbJclQJWhbA2vVnqMZmQbDo+eDtrsGyf1aP6akwNKoZdofItbmo4Yx2cxqzx8zJsO9QVTtB0/DXd5qrj3twVGxAdbv61Vw/rxcSsqg3QvIFNmRjXM3CxCmU58tAL2k60R3PiowXgPMHccWHgj/aHmudyQ3WhXu8KwddO26OLnseqGvoW2tpE3bR19gU2Fj3x5x9ugboy09t2ERi98c5dcXiJHk0gNV8WVRVhFCYmlRgIyBiwDu4q2VPVNOf1ZZhlcQnlySCiOY5SjZQzXrKiLJ4ZYNF9EGApSGWvMiWbL8ogxDpwbFacg9cj2LoZau8DUcT9wxN48907MBrqWC4prRwNhFXNZqkOkyDUf/R4Bu99wfD+torVqu+0VB4rsEdgaT2rHjp3km4VAsFZkM/XNKcp2x2G0Oyb2kT0mGD10AXli0VSllcPbYywYvPtcAa0dCVDrkFnjStb41K1vMpGJQew0pHqtMMR+mECqbK8k5s+Ug6hvlkyBk2nSz31qT6WPNImosWSGxqe7QipVfLAWkxaPcDMAsrl3SmMXyIjLVrJd1hTfpNAazoewC6ZfGzGbU9HBFypOMnYZOT+efT4BL76pa2mvEH/5hgsBikGLK6Dxf/myqPLXL777DSsUEIZlGyuuNaxGHRSQuKwEedECP4ohKP3Y9h+Qdd75sf28PExgdY9eOHaBfkuU9EFddrB0Uy+M18Q6v96DvPDTfQmtvOW9lLXvr9d9an8NhnYXu0z5xoaX25OZpfp66RdTn0CGuYF+PS2htuzFjFfm5zo3FgBDb0GFHrL5qzNEtXoX+UELoNFKeWnnnYKzqmFhR7wasZPgaE11dqjTBPXBnEXQJ2K740QGmXX/jE8o6bloCx5vk5c27RB0zw0wkyUS0vlQMoATt6+ANvf9IhIhM6BY48hM60P37wi3+DCfOzNPzzSO6+ziXf7HSIc7ya1obsxQ8vqmbGnkP/m2NFAqrpw2fRn6CXM2auwKqTOjQjpdDOjWDc0y9BZQeDRvx3AaIdMx62liHsc3nD77j4UeaZTe0qmprnEaQ2SCO6/U8D9L8bgKmnbxjVUm5OtT5VA5ZLze8BPn8q1TqvOb456E4p67YBtMUI3oXODmCMAVIFqBXKocwdHUjAaaTEbL59ypRkBeAoKovPhogOkm3QTG0K1coIJNCQNv4fObTorqO9U3aIlNgZxXVSvJ0CrZm0sgGbNMxe7MwamaYKmX92F1W4ByZV9sX4YZB48OpKrsTnIsZU8T9lk5OIGy5mC+799xavtrscKg5SSzSlCyDmnULFa/XRl2Z8KsOIwVBwQxrmDRRlIcJiclFqarjvREloLYp7v/18x7H0nwtYLGd1YLu7Rt9+fSx4Ti+68yyyXWT58jQDuywn1obkHkyHy2WkbfWQlnzXlLcTfGbbsFuKxaca22qeu6G9wF8Zpl6Mc6pWFeL1C0pyC9sYcVA7vGFq5i03RGc2qE22mo9Wh3oj5Rns3gKOMrkWwq1fYrGuTjuQySWslqj3VHerghUY8mKnl1XMDjcWgzWRwsKt66Ecjrcq3UmMT3A6/fBHGL4cwvvmIZ6nEZN26/RCCKii8lO+EkD4YwewrV6FMQ+hAXYnN1HjAZZO5Xl4gzrlnp2EF2jrWxblQKkiwhzAOsArLd2NwSY2/97kEDi6FsP3yCuKdFSTbJbEsoptHIczvxXD81hDyuXJOFjubHsEd8tkr37Btq7DWLHe3idjQ0PrSrLq3yFWXHTxbAToDYpVb4UNXbFWLTg3g92D6Ythqk3MTroSNotFGCgnWvXibCWeWHzZXgnr9d3Tp1vX0HHTFVzlYF2xq02/YYNMJgA6vYaPUDricExtT1MgNrPdJ2+a5tlhvEDaHlxT9Y1f3G9b39Ib5WzuwvD2BwfUDSC7MAQbHEqa0WkQ0L8ewvHsBiuNRy9TR/wqqogiqKoMnUCcHl6cFE54JYG0PgyoIlMxC1NRRiaZVSmJ0Vwg6a1oPHnEHjKBpt9gCEZrHKOVlL6pFy2pMfBercgrwPd2BNUw9e4UHdDItL2/qXdFw3RbV91JNTauXF9GxkitbkVEe0xI8FRk27XAFj9ZZl6GzGXogWtKXC0is0tDKYcKCJ7C1rrUZcpVZhtgtNVgjEls2h4DmRrn1ShTNB+6zEMyihlibZ8UygsXbl+nVxsLr/akajh1mVOxEK1AnPktZmXXNPHi6n6eCu/UzktQcZlmg3aFBoFqil6Dbs2X4AJW5UqA7kdiV4dBdNQjcqTsGQbHO2lUCudY32PLvzk7wOFNUm4nY0jRnFVG0JA7f17GlTeirvLnWcpp5dxvhHWseNlel1GYtVX+BVjtPUnn7pLUCaqMjcJNaYzxMRx1UbOZj1q5o5ghCLWWnXg/WW456c59Yq8baqUO25Y02bt8svQzoutXuPEaWhgqp2KDZVppDZVIG1S46zwiwFqtSCDTXvooqG5AjX0Vc83aojw+5pPSmp3GzZKMXCXxVSn1yT9fz6CZXbkW/TgaU6pvg3BxAzVXtrEsB+m+snvPWZUY7WR8624sucxQ3k7+hiaGn9rxjkWjmxfUo24xuMd5MNLZyDBEc9eg99ebt+viAjRrw5rnNMsX2o97UpseGRubqU2efA5rPpVESGaGjHhPYdd6hc71GiWxPQr0LdBJquUjHgvOWFE8HWE+ZS1glP8t+OUpSaIL6Pnmqe8sgf94I+rWj+m4fVrE6F1HqYlvdsZi+hKD2rV5dKTxPTok95W98IN26L6On7e6tEB36lKsgYb3sMrZn5VRFBhsKnbGTmBkKsRHNAdzhJdjcHUjZpYSUx91qZiOanj1TOzIcb45wh42mr9zmnDEmsH2Uomn2bvY59Iv+9WutnVPN/gMArybWHjaBzmmsGjyjrOY/4wNXdphnJUwSdeqYe0YmIYpHT2tX2jvIke7CsgIFZ6/W5Svzso5ErXpD1exwxKblCD2LM/QmJT5j0yx24/NV2rGZeFaAQrRMRXfeYQOssIsyurcG6tpBrdWx6GAZXtBCT4ejXYQHN6QaoWWZN02duukFDXbj3lzC3gTCv0uR+fybQ8VT3aFWiKdePgZby8e4rGUnLWtqewYDqxsk6H+2CM5yNX13gWGgqmKuJPA0qDQsKYoQPUPRPSRU4u2ojxalFlMDXcAPVdltYz+xvuUyD6FT72llI2dppleMr0cvuyPiz5Y65NGblGoFlob433ljTaHXTmtTPjPDrm7q0jjB3k/QHce0CU1xuN5VXRpWNe3bUaQQwNhoAqzcwWZlCzAcBE0NThkbm26+ppqgVasbX/ckGqEdLfmgpuzUIqj4qlkYlFN5NuuF2m7TZjqN3wWMVhaRMjh1PbkaK2BiryBLRRwuMA5DOM4yKZTwtDt9PRVg8RDhBOi81BUagmAzSaNAPSG7aUsOrieK1idb05ZRPfQe1bcZ9gfO7XjdX7Ij4tFiXmffXbqeGYjg3ZUH3Hm97TeqjNCH9fbo6N2mCB1MzTwfuliJy+SoLoKOCa08pTxP46xqINX0mtXTfuqJ1spyPtthAJu+xtr1Ns/Onx9YVzHRrgSKYJWAsVcX1W8c1FdDpRyP2WHCOzMjVMtKA569CNDpsV5fjoPIA6VTcjgqPhpsTNmieIaiO0elS7G+UtPcQvYe0w3Kn9iH6dKysEWYr60mqpu/qb5cDx0+AWVPOGxpJzhNQ/NcT5jO48nOVx0SavvJ3GV14CxhGQit7sZeFVXR9azQK5o3r22aXY12GWk0jj0Zob4dmUt4NwsgYqsS7TYnseZ5tN3bjb0R26rboiWqN7ZErRUjtIV59MkwDtPWMg+xoWphY1drEPNPL3YcK8reQtlQNXiGOz+zThWF5u4pJeoqg0GVCP3kM9Ixu12MS/mlGd+4PhPT8o8WLySqPgQJG4HZT8F0VcMExzZNDrv0Q3fQqr3/Zqf+hp5tUOsaiQ+0EBwg0qY7OaUwn6PPr6k1Kqo6t3IG15b13pAX9JVHRhe8Wvl5vtLHniGJ6O1bNEInHFW7m2/4QbgmiCnrM6wIC+MDj/eVlNhCYWbhsyyRLDlGpS7gt75Njr0QxvW022M0qrcjNMMc/CU/Glp9B9Rgn2Y4D1SNtlhhdW42D09Q2cFAPTBDCFS/nuxvlpsAYi/4qrfJ0mxBv9r1zUneKKJsTGIHg7AYlKGhY8d1oQWcHezEmMOOLd3NEANw7lTtZiuWPG6FUbjLTbvYEXhLMJtiPJg16HuMKnSUY15vQcMnPlpmIrpzpYfyWcZh8W45uS4ZITbsOk1oEAVEB0MzmhnPMkm6mJeCRpWz9innBo8zUxnf/fgAtSU59EyMBfzAjNhRN94y9no9B595i2cDQmzPMnd7uzwCP2J7XyE4011sVoROs87N8tBZ194GHvBGtNqVDFp7zhkjtRHv3cCJDusQPTvQ1zYiadBPG9jRDG6tg2S9br9jDmBlefFUEVec0rvnMInR5WWeoei+7gpmWQP2DHBpVaJYw6jUFUfPhEfYTxRu26i+pqIiKkeoA57uRedL22wv947eGJ5mzfYePknfFn9nNk/bt6939lyvRG/Hty0nHp6prY7NXsHydinVC6ix7lU06g2v/3RtNgvG5+aplaU11m/UqgahjLqlzfFxel+OFKK6Id9Wq9+1MDWONhPQET2Wxlr8NxLNa08Wrb0jHePX1s7QcnzUnyHHYoah3t9Bh10piR6IglKXnnlWDIuL8HFJmXEc6Lo31FBmV7IRRVnb4gjPilWqBaxaBKvTqEPloS7q1KPiKhijepk70MKyoCVhulkyprmKn8VkQ4eu4BOtrdAsXwXElmRZX1CjrZV3xm+1JC+u8wzdW5G5wcuRvNNY87HBM1x6uMm8TNPJbTI6o/OdkeRu9mjGYFm6lc2IPIPRgWmOZ2HFnjmfm4/tIThSARo6XP3Ey6ysPIR4mvjMzjjJgCnxGZZIJrNvFEfUsAByDEVw141TOpgUPCSjsyxoBzA5WUuTBZhPRTUXL/UE4nvf8PkOYxTboq88NetUF7R1xGihQ0dz41FH7R1Vn5CqDdZamKo/zWcTKFov5YJ+zVIpU9qzd2EGs8AgWo4EBU3GfBoy4ayQXCsVbQW5GjtPu9ilZ4xgvfAeQoO1bBbcPqClXKRz00YjNxcarLRZJrpLozTZOUezh1x2CitpSDHMFLpWHlcDfFYMi7uFa69nFdPimCyObFUVaKGPZKj+V2gVZTtDMHFTNsEzkxS4gzl7BfViH1G+H63sEuNVl7YGytI+mmCvPJDv1FDatC2P5tOXUPeRAswd49HhybI7Dz17IVoR9PYmp96Hjqan0Ku+m4MAG6K2RwQ/Q6+gJWkA9h2kCE1F3d6n0e4fi+lhV0Z9M0wiK7SuXVb9wBjBDIsj30P1dI7xpwQsECGNS2vksuNzAIM4aBRPfKI8DydDUf7B6K0Bdfapo+Apwg1Un0mpwA5/UAqc2fG9ug59gnBTh1DQ4QfpdfM2UivjHno5EtB2j2PrYl6vbtCYLE3l2ZETj+COb/HsJO1gfa6wikbMBFqCPrSFOqBbvG/0B1qQaKb3tLsysSkbqFpKEPhXLGMYoavyBjT6BlGTFY67kt/0mg4jiIJAvps/ZbD7U5mEi6zAoNAxV1wGlYv3cYlVCR4tPbWCscXCgx4rRqf4DuBNa6l0tlrZciNA3lW65awb4bjJXnvi8mZFM9N5WohhB2j68zjbDGujxlZDPsSOGmGO2EzVrgR4O7OywdDTqaZ/wJesi5sUFNXUDt2dYdbEUmCnB22MrcaOQL6lD2vbdJ0qdT2fTKOaqmoyrjp/Ro933K4wa9mQzixY5Rch6rXQ0Ch9VhXwq+q6r4pSxPbdUQD71dwLgmeY/LzISsUVBMdxKI3R9mogHgIOKFVtZUDPbB66DBlPkJtS0Ax/qA031dQlajnUZ/cRtFhP7pns3zKj0ab28ljtGsMT8aSm9Nd8Tr4kcAdm4BmIrj1BwR/I2PDKtyVDexKc3Z2BzaDR2gDBhlBtxVxhi8lkMRWsCffuKlV2BG27+G2yxU1CPjrN6OZ1nNkBjhJHTgdBPVm8Aq01qPEmFFwQgdP1ntYk/P8EGABtnQ5t6BREwgAAAABJRU5ErkJggg==',
+ 'MVP': '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',
+ 'All-Star': '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',
+ 'Starter': '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',
+ 'Reserve': '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',
+ 'Replacement': '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'
+}
+EXACT_CHANCES = [
+ Decimal('5.7'), Decimal('5.4'), Decimal('5.1'), Decimal('4.8'), Decimal('4.75'), Decimal('4.5'), Decimal('4.25'),
+ Decimal('4.2'), Decimal('3.9'), Decimal('3.8'), Decimal('3.75'), Decimal('3.6'), Decimal('3.5'), Decimal('3.4'),
+ Decimal('3.3'), Decimal('3.25'), Decimal('3.2'), Decimal('2.85'), Decimal('2.8'), Decimal('2.75'), Decimal('2.7'),
+ Decimal('2.6'), Decimal('2.55'), Decimal('2.5'), Decimal('2.4'), Decimal('2.25'), Decimal('2.2'), Decimal('2.1'),
+ Decimal('1.95'), Decimal('1.9'), Decimal('1.8'), Decimal('1.75'), Decimal('1.7'), Decimal('1.65'), Decimal('1.6'),
+ Decimal('1.5'), Decimal('1.4'), Decimal('1.35'), Decimal('1.3'), Decimal('1.25'), Decimal('1.2'), Decimal('1.1'),
+ Decimal('1.05')
+]
+
+
+class PlayResult(pydantic.BaseModel):
+ full_name: str
+ short_name: str
+ is_offense: bool = True
+
+ @validator("is_offense", always=True)
+ def offense_validator(cls, v, values, **kwargs):
+ return values['short_name'][:2] in ['HR', 'TR', 'DO', 'SI', 'WA', 'HB', '◆B']
+
+
+PLAY_RESULTS = {
+ 'hr': PlayResult(full_name='HOMERUN', short_name='HR'),
+ 'bp-hr': PlayResult(full_name='◆BP-HR', short_name='◆BP-HR'),
+ 'tr': PlayResult(full_name='TRIPLE', short_name='TR'),
+ 'do-lf': PlayResult(full_name=f'DOUBLE (lf)', short_name=f'DO (lf)'),
+ 'do-cf': PlayResult(full_name=f'DOUBLE (cf)', short_name=f'DO (cf)'),
+ 'do-rf': PlayResult(full_name=f'DOUBLE (rf)', short_name=f'DO (rf)'),
+ 'do***': PlayResult(full_name=f'DOUBLE***', short_name=f'DO***'),
+ 'do**': PlayResult(full_name=f'DOUBLE**', short_name=f'DO**'),
+ 'si**': PlayResult(full_name='SINGLE**', short_name='SI**'),
+ 'si*': PlayResult(full_name='SINGLE*', short_name='SI*'),
+ 'si-cf': PlayResult(full_name='SINGLE (cf)', short_name='SI (cf)'),
+ 'bp-si': PlayResult(full_name='▼BP-SI', short_name='◆BP-SI'),
+ 'walk': PlayResult(full_name='WALK', short_name='WALK'),
+ 'fly-rf': PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'),
+ 'fly-lf': PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'),
+ 'fly-cf': PlayResult(full_name=f'fly (cf) B', short_name=f'fly (cf) B'),
+ 'fly-bq': PlayResult(full_name=f'fly B?', short_name=f'fly B?')
+}
+
+
+class BattingCardRatingsModel(pydantic.BaseModel):
+ battingcard: int
+ vs_hand: Literal['R', 'L']
+ pull_rate: Decimal = Decimal(0.0)
+ center_rate: Decimal = Decimal(0.0)
+ slap_rate: Decimal = Decimal(0.0)
+ homerun: Decimal = Decimal(0.0)
+ bp_homerun: Decimal = Decimal(0.0)
+ triple: Decimal = Decimal(0.0)
+ double_three: Decimal = Decimal(0.0)
+ double_two: Decimal = Decimal(0.0)
+ double_pull: Decimal = Decimal(0.0)
+ single_two: Decimal = Decimal(0.0)
+ single_one: Decimal = Decimal(0.0)
+ single_center: Decimal = Decimal(0.0)
+ bp_single: Decimal = Decimal(0.0)
+ hbp: Decimal = Decimal(0.0)
+ walk: Decimal = Decimal(0.0)
+ strikeout: Decimal = Decimal(0.0)
+ lineout: Decimal = Decimal(0.0)
+ popout: Decimal = Decimal(0.0)
+ flyout_a: Decimal = Decimal(0.0)
+ flyout_bq: Decimal = Decimal(0.0)
+ flyout_lf_b: Decimal = Decimal(0.0)
+ flyout_rf_b: Decimal = Decimal(0.0)
+ groundout_a: Decimal = Decimal(0.0)
+ groundout_b: Decimal = Decimal(0.0)
+ groundout_c: Decimal = Decimal(0.0)
+ avg: Decimal = Decimal(0.0)
+ obp: Decimal = Decimal(0.0)
+ slg: Decimal = Decimal(0.0)
+
+ def total_chances(self):
+ return Decimal(sum([
+ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull,
+ self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout,
+ self.lineout, self.popout, self.flyout_a, self.flyout_bq, self.flyout_lf_b, self.flyout_rf_b,
+ self.groundout_a, self.groundout_b, self.groundout_c
+ ]))
+
+ def update_slash_lines(self):
+ self.avg = Decimal(
+ (self.homerun + self.bp_homerun / 2 + self.triple + self.double_three +
+ self.double_two + self.double_pull + self.single_two + self.single_one +
+ self.single_center + self.bp_single / 2) / Decimal(108)
+ )
+
+ self.obp = Decimal(((self.hbp + self.walk) / 108) + self.avg)
+
+ self.slg = Decimal(
+ (self.homerun * 4 + self.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 +
+ self.double_two * 2 + self.double_pull * 2 + self.single_two + self.single_one +
+ self.single_center + self.bp_single / 2) / 108
+ )
+
+
+class PitchingCardRatingsModel(pydantic.BaseModel):
+ pitchingcard: int
+ vs_hand: Literal['R', 'L']
+ homerun: Decimal = Decimal(0.0)
+ bp_homerun: Decimal = Decimal(0.0)
+ triple: Decimal = Decimal(0.0)
+ double_three: Decimal = Decimal(0.0)
+ double_two: Decimal = Decimal(0.0)
+ double_cf: Decimal = Decimal(0.0)
+ single_two: Decimal = Decimal(0.0)
+ single_one: Decimal = Decimal(0.0)
+ single_center: Decimal = Decimal(0.0)
+ bp_single: Decimal = Decimal(0.0)
+ hbp: Decimal = Decimal(0.0)
+ walk: Decimal = Decimal(0.0)
+ strikeout: Decimal = Decimal(0.0)
+ flyout_lf_b: Decimal = Decimal(0.0)
+ flyout_cf_b: Decimal = Decimal(0.0)
+ flyout_rf_b: Decimal = Decimal(0.0)
+ groundout_a: Decimal = Decimal(0.0)
+ groundout_b: Decimal = Decimal(0.0)
+ xcheck_p: Decimal = Decimal(0.0)
+ xcheck_c: Decimal = Decimal(0.0)
+ xcheck_1b: Decimal = Decimal(0.0)
+ xcheck_2b: Decimal = Decimal(0.0)
+ xcheck_3b: Decimal = Decimal(0.0)
+ xcheck_ss: Decimal = Decimal(0.0)
+ xcheck_lf: Decimal = Decimal(0.0)
+ xcheck_cf: Decimal = Decimal(0.0)
+ xcheck_rf: Decimal = Decimal(0.0)
+ avg: Decimal = Decimal(0.0)
+ obp: Decimal = Decimal(0.0)
+ slg: Decimal = Decimal(0.0)
+
+ def total_chances(self):
+ return Decimal(sum([
+ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf,
+ self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout,
+ self.flyout_lf_b, self.flyout_cf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.xcheck_p,
+ self.xcheck_c, self.xcheck_1b, self.xcheck_2b, self.xcheck_3b, self.xcheck_ss, self.xcheck_lf,
+ self.xcheck_cf, self.xcheck_rf
+ ]))
+
+ def update_slash_lines(self):
+ self.avg = Decimal(
+ (self.homerun + self.bp_homerun / 2 + self.triple + self.double_three +
+ self.double_two + self.double_cf + self.single_two + self.single_one +
+ self.single_center + self.bp_single / 2) / Decimal(108)
+ )
+
+ self.obp = Decimal(((self.hbp + self.walk) / 108) + self.avg)
+
+ self.slg = Decimal(
+ (self.homerun * 4 + self.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 +
+ self.double_two * 2 + self.double_cf * 2 + self.single_two + self.single_one +
+ self.single_center + self.bp_single / 2) / 108)
+
+
+class CardResult(pydantic.BaseModel):
+ result_one: str = None
+ result_two: str = None
+ d20_one: str = None
+ d20_two: str = None
+ bold_one: bool = False
+ bold_two: bool = False
+
+ def __str__(self):
+ res_text = f'Empty'
+ if self.result_one is not None:
+ res_text = f'{self.result_one}'
+ if self.d20_one is not None:
+ res_text += f' | {self.d20_one}'
+ if self.result_two is not None:
+ res_text += f'\n{self.result_two} | {self.d20_two}'
+ return res_text
+
+ def is_full(self):
+ return self.result_one is not None
+
+ def assign_play(self, play: PlayResult, secondary_play: Optional[PlayResult] = None, d20: Optional[int] = None):
+ if secondary_play is None:
+ self.result_one = play.full_name
+ if '++' in play.full_name:
+ logging.warning(f'Too many plus symbols: {play.full_name}')
+ self.result_one = re.sub(r'\++', '+', play.full_name)
+
+ if play.is_offense:
+ self.bold_one = True
+ else:
+ self.result_one = play.short_name
+ self.result_two = secondary_play.short_name
+ self.d20_one = f'1-{d20}'
+ if d20 == 19:
+ self.d20_two = f'20'
+ else:
+ self.d20_two = f'{d20 + 1}-20'
+
+ if play.is_offense:
+ self.bold_one = True
+ if secondary_play.is_offense:
+ self.bold_two = True
+
+ logging.debug(f'this result: {self}')
+
+
+class CardColumn(pydantic.BaseModel):
+ two: CardResult = CardResult() # 1 chance
+ three: CardResult = CardResult() # 2 chances
+ four: CardResult = CardResult() # 3 chances
+ five: CardResult = CardResult() # 4 chances
+ six: CardResult = CardResult() # 5 chances
+ seven: CardResult = CardResult() # 6 chances
+ eight: CardResult = CardResult() # 5 chances
+ nine: CardResult = CardResult() # 4 chances
+ ten: CardResult = CardResult() # 3 chances
+ eleven: CardResult = CardResult() # 2 chances
+ twelve: CardResult = CardResult() # 1 chance
+ num_splits: int = 0
+ num_lomax: int = 0
+ num_plusgb: int = 0
+
+ def __str__(self):
+ return f'2-{self.two}\n' \
+ f'3-{self.three}\n' \
+ f'4-{self.four}\n' \
+ f'5-{self.five}\n' \
+ f'6-{self.six}\n' \
+ f'7-{self.seven}\n' \
+ f'8-{self.eight}\n' \
+ f'9-{self.nine}\n' \
+ f'10-{self.ten}\n' \
+ f'11-{self.eleven}\n' \
+ f'12-{self.twelve}'
+
+ def get_text(self):
+ sixes = f''
+ results = f''
+ d20 = f''
+
+ def bold(text):
+ return f'{text}'
+
+ def blank():
+ return f' '
+
+ for count, x in enumerate(
+ [self.two, self.three, self.four, self.five, self.six, self.seven, self.eight, self.nine,
+ self.ten, self.eleven, self.twelve], start=2):
+ if x.bold_one:
+ this_six = bold(f'{count}-')
+ this_result = bold(x.result_one)
+ if x.d20_one is not None:
+ this_d20 = bold(x.d20_one)
+ else:
+ this_d20 = blank()
+ else:
+ this_six = f'{count}-'
+ this_result = f'{x.result_one}'
+ if x.d20_one is not None:
+ this_d20 = f'{x.d20_one}'
+ else:
+ this_d20 = blank()
+
+ if x.result_two is not None:
+ if x.bold_two:
+ this_six += f'
{bold(blank())}'
+ this_result += f'
{bold(x.result_two)}'
+ this_d20 += f'
{bold(x.d20_two)}'
+ else:
+ this_six += f'
{blank()}'
+ this_result += f'
{x.result_two}'
+ this_d20 += f'
{x.d20_two}'
+
+ sixes += f'{this_six}
'
+ results += f'{this_result}
'
+ d20 += f'{this_d20}
'
+
+ return {
+ 'sixes': sixes,
+ 'results': results,
+ 'd20': d20
+ }
+
+ def is_full(self):
+ return self.two.is_full() and self.three.is_full() and self.four.is_full() and self.five.is_full() and \
+ self.six.is_full() and self.seven.is_full() and self.eight.is_full() and self.nine.is_full() and \
+ self.ten.is_full() and self.eleven.is_full() and self.twelve.is_full()
+
+ def add_result(
+ self, play: PlayResult, alt_direction: int, chances: Decimal, secondary_play: Optional[PlayResult] = None):
+ if chances > Decimal(6.0):
+ logging.error(f'Cannot assign more than 6 chances per call\n'
+ f'Play: {play}\nAlt Direction: {alt_direction}\nChances: {chances}\n'
+ f'Secondary Play: {secondary_play}')
+ raise ValueError(f'Cannot assign more than 6 chances per call')
+ elif math.floor(chances) != chances and secondary_play is None:
+ if chances > Decimal(1.0):
+ chances = Decimal(math.floor(chances))
+ else:
+ logging.error(f'Must have secondary play for fractional chances; could not round down to an integer\n'
+ f'Play: {play}\nChances: {chances}\nSecondary Play: {secondary_play}')
+ return False
+
+ # Chances is whole number
+ if math.floor(chances) == chances:
+ if chances == Decimal(6):
+ if not self.seven.is_full():
+ self.seven.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.six.is_full():
+ if not self.two.is_full():
+ self.six.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.six.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.eight.is_full():
+ if not self.two.is_full():
+ self.eight.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.eight.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ # Plus two
+ if not self.five.is_full():
+ if not self.three.is_full():
+ self.five.assign_play(play)
+ self.three.assign_play(play)
+ return chances, 0
+ elif not self.eleven.is_full():
+ self.five.assign_play(play)
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ # Bulk 2, 3, 4 and 10, 11, 12
+ if not self.three.is_full() and not self.two.is_full() and not self.four.is_full():
+ self.four.assign_play(play)
+ self.three.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+
+ if not self.ten.is_full() and not self.eleven.is_full() and not self.twelve.is_full():
+ self.ten.assign_play(play)
+ self.eleven.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ if not self.nine.is_full():
+ if not self.three.is_full():
+ self.nine.assign_play(play)
+ self.three.assign_play(play)
+ return chances, 0
+ elif not self.eleven.is_full():
+ self.nine.assign_play(play)
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ if chances == Decimal(5):
+ if not self.six.is_full():
+ self.six.assign_play(play)
+ return chances, 0
+
+ if not self.eight.is_full():
+ self.eight.assign_play(play)
+ return chances, 0
+
+ # Bulk 3, 4 and 10, 11
+ if not self.three.is_full() and not self.four.is_full():
+ self.four.assign_play(play)
+ self.three.assign_play(play)
+ return chances, 0
+
+ if not self.ten.is_full() and not self.eleven.is_full():
+ self.ten.assign_play(play)
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.five.is_full():
+ if not self.two.is_full():
+ self.five.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.five.result_one = play.full_name
+ self.twelve.result_one = play.full_name
+ return chances, 0
+
+ # Plus one
+ if not self.nine.is_full():
+ if not self.two.is_full():
+ self.nine.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.nine.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ # Plus two
+ if not self.four.is_full():
+ if not self.three.is_full():
+ self.four.assign_play(play)
+ self.three.assign_play(play)
+ return chances, 0
+ elif not self.eleven.is_full():
+ self.four.assign_play(play)
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ # Plus two
+ if not self.ten.is_full():
+ if not self.three.is_full():
+ self.ten.assign_play(play)
+ self.three.assign_play(play)
+ return chances, 0
+ elif not self.eleven.is_full():
+ self.ten.assign_play(play)
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ if chances == Decimal(4):
+ if not self.five.is_full():
+ self.five.assign_play(play)
+ return chances, 0
+
+ if not self.nine.is_full():
+ self.nine.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.four.is_full():
+ if not self.two.is_full():
+ self.four.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.four.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.ten.is_full():
+ if not self.two.is_full():
+ self.ten.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.ten.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ if not self.three.is_full() and not self.eleven.is_full():
+ self.three.assign_play(play)
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ if chances == Decimal(3):
+ if not self.four.is_full():
+ self.four.assign_play(play)
+ return chances, 0
+
+ if not self.ten.is_full():
+ self.ten.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.three.is_full():
+ if not self.two.is_full():
+ self.three.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+ elif not self.twelve.is_full():
+ self.three.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ # Plus one
+ if not self.eleven.is_full():
+ if not self.twelve.is_full():
+ self.eleven.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+ if not self.two.is_full():
+ self.eleven.assign_play(play)
+ self.two.assign_play(play)
+ return chances, 0
+
+ if chances == Decimal(2):
+ if not self.three.is_full():
+ self.three.assign_play(play)
+ return chances, 0
+
+ if not self.eleven.is_full():
+ self.eleven.assign_play(play)
+ return chances, 0
+
+ if not self.two.is_full() and not self.twelve.is_full():
+ self.two.assign_play(play)
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ if chances == Decimal(1):
+ if not self.two.is_full():
+ self.two.assign_play(play)
+ return chances, 0
+
+ if not self.twelve.is_full():
+ self.twelve.assign_play(play)
+ return chances, 0
+
+ return False
+
+ logging.info(f'Not a whole number | Chances: {chances}')
+ if chances in EXACT_CHANCES and self.num_splits < 4 and secondary_play is not None:
+ logging.info(f'In Exact Chances!')
+ if chances >= 3:
+ self.num_splits += 1
+ logging.info(f'Chances is greater than 3')
+ if chances == Decimal('3.2'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 16)
+ return chances, Decimal('0.8')
+ elif not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 16)
+ return chances, Decimal('0.8')
+ elif chances == Decimal('3.25'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.75')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.75')
+ elif chances == Decimal('3.3') and not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 11)
+ return chances, Decimal('2.7')
+ elif chances == Decimal('3.4'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.6')
+ elif not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.6')
+ elif chances == Decimal('3.5'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 14)
+ return chances, Decimal('1.5')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 14)
+ return chances, Decimal('1.5')
+ elif chances == Decimal('3.6'):
+ if not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.4')
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.4')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 12)
+ return chances, Decimal('2.4')
+ elif chances == Decimal('3.75'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 15)
+ return chances, Decimal('1.25')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 15)
+ return chances, Decimal('1.25')
+ elif chances == Decimal('3.8'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.2')
+ elif not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.2')
+ elif chances == Decimal('3.9'):
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 13)
+ return chances, Decimal('2.1')
+ elif chances == Decimal('4.2'):
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 14)
+ return chances, Decimal('1.8')
+ elif chances == Decimal('4.25'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.75')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.75')
+ elif chances == Decimal('4.5'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.5')
+ if not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.5')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 15)
+ return chances, Decimal('1.5')
+ elif chances == Decimal('4.75'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.25')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.25')
+ elif chances == Decimal('4.8'):
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 16)
+ return chances, Decimal('1.2')
+ elif chances == Decimal('5.1'):
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.9')
+ elif chances == Decimal('5.4'):
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.6')
+ elif chances == Decimal('5.7'):
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.3')
+ elif chances >= 1:
+ self.num_splits += 1
+ logging.info(f'Chances is greater than 1')
+ if chances == Decimal('1.05'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 7)
+ return chances, Decimal('1.95')
+ elif not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 7)
+ return chances, Decimal('1.95')
+ if chances == Decimal('1.1'):
+ if not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 11)
+ return chances, Decimal('0.9')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 11)
+ return chances, Decimal('0.9')
+ if chances == Decimal('1.2'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 6)
+ return chances, Decimal('2.8')
+ elif not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 6)
+ return chances, Decimal('2.8')
+ elif not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 8)
+ return chances, Decimal('1.8')
+ elif not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 8)
+ return chances, Decimal('1.8')
+ elif not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 12)
+ return chances, Decimal('0.8')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 12)
+ return chances, Decimal('0.8')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 4)
+ return chances, Decimal('4.8')
+ if chances == Decimal('1.25'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 5)
+ return chances, Decimal('3.75')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 5)
+ return chances, Decimal('3.75')
+ if chances == Decimal('1.3'):
+ if not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 13)
+ return chances, Decimal('0.7')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 13)
+ return chances, Decimal('0.7')
+ if chances == Decimal('1.35'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 9)
+ return chances, Decimal('1.65')
+ elif not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 9)
+ return chances, Decimal('1.65')
+ if chances == Decimal('1.4'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 7)
+ return chances, Decimal('2.6')
+ elif not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 7)
+ return chances, Decimal('2.6')
+ elif not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 14)
+ return chances, Decimal('0.6')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 14)
+ return chances, Decimal('0.6')
+ if chances == Decimal('1.5'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 6)
+ return chances, Decimal('3.5')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 6)
+ return chances, Decimal('3.5')
+ elif not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 10)
+ return chances, Decimal('1.5')
+ elif not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 10)
+ return chances, Decimal('1.5')
+ elif not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 15)
+ return chances, Decimal('0.5')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 15)
+ return chances, Decimal('0.5')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 5)
+ return chances, Decimal('4.5')
+ if chances == Decimal('1.6'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 8)
+ return chances, Decimal('2.4')
+ elif not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 8)
+ return chances, Decimal('2.4')
+ elif not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 16)
+ return chances, Decimal('0.4')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 16)
+ return chances, Decimal('0.4')
+ if chances == Decimal('1.65'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 11)
+ return chances, Decimal('1.35')
+ elif not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 11)
+ return chances, Decimal('1.35')
+ if chances == Decimal('1.7'):
+ if not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.3')
+ elif not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.3')
+ if chances == Decimal('1.75'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 7)
+ return chances, Decimal('3.25')
+ elif not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 7)
+ return chances, Decimal('3.25')
+ if chances == Decimal('1.8'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 9)
+ return chances, Decimal('2.2')
+ if not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 9)
+ return chances, Decimal('2.2')
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 12)
+ return chances, Decimal('1.2')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 12)
+ return chances, Decimal('1.2')
+ if not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.2')
+ if not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.2')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 6)
+ return chances, Decimal('4.2')
+ if chances == Decimal('1.9'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.1')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.1')
+ if not self.three.is_full():
+ self.three.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.1')
+ if not self.eleven.is_full():
+ self.eleven.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.1')
+ if chances == Decimal('1.95'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.05')
+ elif not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.05')
+ if chances == Decimal('2.1'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 14)
+ return chances, Decimal('0.9')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 14)
+ return chances, Decimal('0.9')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 7)
+ return chances, Decimal('3.9')
+ if chances == Decimal('2.2'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 11)
+ return chances, Decimal('1.8')
+ if not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 11)
+ return chances, Decimal('1.8')
+ if chances == Decimal('2.25'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 9)
+ return chances, Decimal('2.75')
+ if not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 9)
+ return chances, Decimal('2.75')
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 15)
+ return chances, Decimal('0.75')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 15)
+ return chances, Decimal('0.75')
+ if chances == Decimal('2.4'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 12)
+ return chances, Decimal('1.6')
+ if not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 12)
+ return chances, Decimal('1.6')
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 16)
+ return chances, Decimal('0.6')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 16)
+ return chances, Decimal('0.6')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 8)
+ return chances, Decimal('3.6')
+ if chances == Decimal('2.5'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 10)
+ return chances, Decimal('2.5')
+ if not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 10)
+ return chances, Decimal('2.5')
+ if chances == Decimal('2.55'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.45')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 17)
+ return chances, Decimal('0.45')
+ if chances == Decimal('2.6'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.4')
+ if not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 13)
+ return chances, Decimal('1.4')
+ if chances == Decimal('2.7'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.3')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 18)
+ return chances, Decimal('0.3')
+ if not self.seven.is_full():
+ self.seven.assign_play(play, secondary_play, 9)
+ return chances, Decimal('3.3')
+ if chances == Decimal('2.75'):
+ if not self.six.is_full():
+ self.six.assign_play(play, secondary_play, 11)
+ return chances, Decimal('2.25')
+ if not self.eight.is_full():
+ self.eight.assign_play(play, secondary_play, 11)
+ return chances, Decimal('2.25')
+ if chances == Decimal('2.8'):
+ if not self.five.is_full():
+ self.five.assign_play(play, secondary_play, 14)
+ return chances, Decimal('1.2')
+ if not self.nine.is_full():
+ self.nine.assign_play(play, secondary_play, 14)
+ return chances, Decimal('1.2')
+ if chances == Decimal('2.85'):
+ if not self.four.is_full():
+ self.four.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.15')
+ if not self.ten.is_full():
+ self.ten.assign_play(play, secondary_play, 19)
+ return chances, Decimal('0.15')
+ else:
+ logging.info(f'Chances is less than 1')
+ return False
+
+ self.num_splits -= 1
+
+ else:
+ logging.info(f'Not a whole number and not in Exact Chances! Trying to add a subset')
+ for x in EXACT_CHANCES:
+ if x < chances and ((chances - x) == round(chances - x)):
+ logging.info(f'Trying to add {x} chances')
+ return self.add_result(play, alt_direction, x, secondary_play)
+ logging.info(f'Could not find a valid match')
+ return False
+
+ def total_chances(self):
+ total = 0
+ total += 1 if self.two.is_full() else 0
+ total += 2 if self.three.is_full() else 0
+ total += 3 if self.four.is_full() else 0
+ total += 4 if self.five.is_full() else 0
+ total += 5 if self.six.is_full() else 0
+ total += 6 if self.seven.is_full() else 0
+ total += 5 if self.eight.is_full() else 0
+ total += 4 if self.nine.is_full() else 0
+ total += 3 if self.ten.is_full() else 0
+ total += 2 if self.eleven.is_full() else 0
+ total += 1 if self.twelve.is_full() else 0
+ return total
+
+ def add_fatigue(self, num_chances: int, k_only: bool = False):
+ def is_valid_result(this_result: CardResult):
+ if k_only:
+ if this_result.result_one == 'strikeout':
+ return True
+ else:
+ return False
+ else:
+ if this_result.result_two is None and not this_result.bold_one and 'X' not in this_result.result_one:
+ return True
+ return False
+
+ if num_chances == 6:
+ if is_valid_result(self.seven):
+ self.seven.result_one += ' •'
+ return 6
+ elif num_chances == 5:
+ if is_valid_result(self.six):
+ self.six.result_one += ' •'
+ return 5
+ if is_valid_result(self.eight):
+ self.eight.result_one += ' •'
+ return 5
+
+ elif num_chances == 4:
+ if is_valid_result(self.five):
+ self.five.result_one += ' •'
+ return 4
+ if is_valid_result(self.nine):
+ self.nine.result_one += ' •'
+ return 4
+
+ return 0
+
+
+class FullCard(pydantic.BaseModel):
+ col_one: CardColumn = CardColumn()
+ col_two: CardColumn = CardColumn()
+ col_three: CardColumn = CardColumn()
+ offense_col: int
+ alt_direction: int = 1
+ num_plusgb: int = 0
+ num_lomax: int = 0
+ is_batter: bool = False
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ def get_columns(self, is_offense: bool):
+ if is_offense:
+ if self.offense_col == 1:
+ first = self.col_one
+ if self.alt_direction:
+ second = self.col_two
+ third = self.col_three
+ else:
+ second = self.col_three
+ third = self.col_two
+ elif self.offense_col == 2:
+ first = self.col_two
+ if self.alt_direction:
+ second = self.col_three
+ third = self.col_one
+ else:
+ second = self.col_one
+ third = self.col_three
+ else:
+ first = self.col_three
+ if self.alt_direction:
+ second = self.col_one
+ third = self.col_two
+ else:
+ second = self.col_two
+ third = self.col_one
+ else:
+ if self.offense_col == 1:
+ third = self.col_one
+ if self.alt_direction:
+ first = self.col_two
+ second = self.col_three
+ else:
+ first = self.col_three
+ second = self.col_two
+ elif self.offense_col == 2:
+ third = self.col_two
+ if self.alt_direction:
+ first = self.col_three
+ second = self.col_one
+ else:
+ first = self.col_one
+ second = self.col_three
+ else:
+ third = self.col_three
+ if self.alt_direction:
+ first = self.col_one
+ second = self.col_two
+ else:
+ first = self.col_two
+ second = self.col_one
+
+ return first, second, third
+
+ def is_complete(self):
+ return self.col_one.is_full() and self.col_two.is_full() and self.col_three.is_full()
+
+ def sample_output(self):
+ return f'{"" if self.is_complete() else "NOT "}COMPLETE\n' \
+ f'Column 1\n{self.col_one}\n\n' \
+ f'Column 2\n{self.col_two}\n\n' \
+ f'Column 3\n{self.col_three}'
+
+ def add_result(self, play: PlayResult, chances: Decimal, secondary_play: Optional[PlayResult] = None):
+ first, second, third = self.get_columns(is_offense=play.is_offense)
+
+ if 'gb' in play.full_name and chances + self.num_plusgb <= 6 and self.is_batter:
+ play.full_name += '+'
+
+ for x in [first, second, third]:
+ r_data = x.add_result(play, self.alt_direction, chances, secondary_play)
+ if r_data:
+ if '+' in play.full_name:
+ self.num_plusgb += r_data[0]
+ elif 'max' in play.full_name:
+ self.num_lomax += r_data[0]
+ return r_data
+
+ return False
+
+ def card_fill(self, play: PlayResult):
+ for x in range(6, 0, -1):
+ r_data = self.add_result(play, Decimal(x))
+ if r_data:
+ return r_data
+
+ return 0, 0
+
+ def card_output(self):
+ c1_output = self.col_one.get_text()
+ c2_output = self.col_two.get_text()
+ c3_output = self.col_three.get_text()
+
+ return {
+ 'one_2d6': c1_output['sixes'],
+ 'one_results': c1_output['results'],
+ 'one_d20': c1_output['d20'],
+ 'two_2d6': c2_output['sixes'],
+ 'two_results': c2_output['results'],
+ 'two_d20': c2_output['d20'],
+ 'three_2d6': c3_output['sixes'],
+ 'three_results': c3_output['results'],
+ 'three_d20': c3_output['d20']
+ }
+
+ def total_chances(self):
+ return self.col_one.total_chances() + self.col_two.total_chances() + self.col_three.total_chances()
+
+ def add_fatigue(self):
+ first, second, third = self.get_columns(is_offense=False)
+
+ total_added = 0
+ for x in [first, second, third]:
+ resp = x.add_fatigue(6, k_only=True)
+ if resp:
+ total_added += resp
+ break
+
+ if total_added == 0:
+ for x in [first, second, third]:
+ resp = x.add_fatigue(6, k_only=False)
+ if resp:
+ total_added += resp
+ break
+
+ if total_added == 0:
+ for x in [first, second, third]:
+ resp = x.add_fatigue(5, k_only=True)
+ if resp:
+ total_added += resp
+ break
+
+ if total_added == 0:
+ for x in [first, second, third]:
+ resp = x.add_fatigue(5, k_only=False)
+ if resp:
+ total_added += resp
+ break
+
+ if total_added != 10:
+ for x in [first, second, third]:
+ resp = x.add_fatigue(10 - total_added, k_only=True)
+ if resp:
+ total_added += resp
+ break
+
+ if total_added != 10:
+ for x in [first, second, third]:
+ resp = x.add_fatigue(10 - total_added, k_only=False)
+ if resp:
+ total_added += resp
+ break
+
+ if total_added != 10:
+ logging.error(f'FullCard add_fatigue - Could not add all fatigue results / total_added: {total_added}')
+
+
+class FullBattingCard(FullCard):
+ ratings: BattingCardRatingsModel
+ is_batter: bool = True
+
+
+class FullPitchingCard(FullCard):
+ ratings: PitchingCardRatingsModel
+ is_batter: bool = False
+
+
+def get_pos_data(all_pos, is_pitcher: bool = False) -> dict:
+ final = ''
+ arm_added = False
+ first = True
+ for x in all_pos:
+ if not first:
+ final += ', '
+
+ first = False
+ if is_pitcher:
+ if x.position == 'P':
+ final += f'p-{x.range}e{x.error}'
+ else:
+ if x.position != 'P':
+ final += x.position.lower()
+ if x.position != 'DH':
+ final += f'-{x.range}'
+ if x.position in ['LF', 'CF', 'RF', 'C'] and not arm_added:
+ final += f'({"+" if x.arm >= 0 else ""}{x.arm})'
+ arm_added = True
+
+ final += f'e{x.error}'
+ if x.position == 'C':
+ final += f' T-{x.overthrow}(pb {x.pb})'
+
+ if len(final) >= 50:
+ font = 16
+ margin = 5
+ elif len(final) >= 46:
+ font = 18
+ margin = 3
+ elif len(final) >= 40:
+ font = 20
+ margin = 1
+ elif len(final) >= 35:
+ font = 22
+ margin = -1
+ elif len(final) >= 30:
+ font = 24
+ margin = -2
+ else:
+ font = 26
+ margin = -3
+
+ return {'string': final, 'font': font, 'margin': margin}
+
+
+def full_log(this_ratings, this_card, info=False):
+ if info:
+ logging.info(
+ f'Rating Chances: {this_ratings.total_chances()} / Card Chances: {this_card.total_chances()}\n'
+ f'{this_card.sample_output()}\n'
+ )
+ else:
+ logging.debug(
+ f'Rating Chances: {this_ratings.total_chances()} / Card Chances: {this_card.total_chances()}\n'
+ f'{this_card.sample_output()}\n'
+ )
+
+
+def get_chances(total_chances, apply_limits=True):
+ if total_chances > 12.5 and apply_limits:
+ return 6
+ elif total_chances > 10.5 and apply_limits:
+ return 5
+ elif total_chances > 8.5 and apply_limits:
+ return 4
+ elif total_chances > 5.5 and apply_limits:
+ return 3
+ else:
+ return min(total_chances, 6)
+
+
+def get_batter_card_data(player, batting_card, ratings_vl, ratings_vr, positions) -> dict:
+ player_binary = batting_card.player_id % 2
+ logging.info(f'\n\nRunning Card for {player.p_name}')
+ steal_string = '-/- (---)'
+ if batting_card.steal_jump > 0:
+ jump_chances = round(batting_card.steal_jump * 36)
+
+ if jump_chances == 6:
+ good_jump = 7
+ elif jump_chances == 5:
+ good_jump = 6
+ elif jump_chances == 4:
+ good_jump = 5
+ elif jump_chances == 3:
+ good_jump = 4
+ elif jump_chances == 2:
+ good_jump = 3
+ elif jump_chances == 1:
+ good_jump = 2
+ elif jump_chances == 7:
+ good_jump = '4,5'
+ elif jump_chances == 8:
+ good_jump = '4,6'
+ elif jump_chances == 9:
+ good_jump = '3-5'
+ elif jump_chances == 10:
+ good_jump = '2-5'
+ elif jump_chances == 11:
+ good_jump = '6,7'
+ elif jump_chances == 12:
+ good_jump = '4-6'
+ elif jump_chances == 13:
+ good_jump = '2,4-6'
+ elif jump_chances == 14:
+ good_jump = '3-6'
+ elif jump_chances == 15:
+ good_jump = '2-6'
+ elif jump_chances == 16:
+ good_jump = '2,5-6'
+ elif jump_chances == 17:
+ good_jump = '3,5-6'
+ elif jump_chances == 18:
+ good_jump = '4-6'
+ elif jump_chances == 19:
+ good_jump = '2,4-7'
+ elif jump_chances == 20:
+ good_jump = '3-7'
+ elif jump_chances == 21:
+ good_jump = '2-7'
+ elif jump_chances == 22:
+ good_jump = '2-7,12'
+ elif jump_chances == 23:
+ good_jump = '2-7,11'
+ elif jump_chances == 24:
+ good_jump = '2,4-8'
+ elif jump_chances == 25:
+ good_jump = '3-8'
+ elif jump_chances == 26:
+ good_jump = '2-8'
+ elif jump_chances == 27:
+ good_jump = '2-8,12'
+ elif jump_chances == 28:
+ good_jump = '2-8,11'
+ elif jump_chances == 29:
+ good_jump = '3-9'
+ elif jump_chances == 30:
+ good_jump = '2-9'
+ elif jump_chances == 31:
+ good_jump = '2-9,12'
+ elif jump_chances == 32:
+ good_jump = '2-9,11'
+ elif jump_chances == 33:
+ good_jump = '2-10'
+ elif jump_chances == 34:
+ good_jump = '3-11'
+ elif jump_chances == 35:
+ good_jump = '2-11'
+ else:
+ good_jump = '2-12'
+ steal_string = f'{"*" if batting_card.steal_auto else ""}{good_jump}/- ({batting_card.steal_high}-' \
+ f'{batting_card.steal_low})'
+
+ rarity_file = encoded_images[player.rarity.name]
+
+ vl_dict = model_to_dict(ratings_vl)
+ vl_dict['battingcard'] = ratings_vl.battingcard_id
+ vl_ratings = BattingCardRatingsModel(**vl_dict)
+ vl = FullBattingCard(
+ ratings=vl_ratings,
+ offense_col=batting_card.offense_col,
+ alt_direction=player_binary
+ )
+ vr_dict = model_to_dict(ratings_vr)
+ vr_dict['battingcard'] = ratings_vr.battingcard_id
+ vr_ratings = BattingCardRatingsModel(**vr_dict)
+ vr = FullBattingCard(
+ ratings=vr_ratings,
+ offense_col=batting_card.offense_col,
+ alt_direction=player_binary
+ )
+
+ def assign_bchances(
+ this_card: FullBattingCard, play: PlayResult, chances: Decimal,
+ secondary_play: Optional[PlayResult] = None):
+ logging.info(f'Assign batting chances\n{play}\nChances: {chances}\nBackup: {secondary_play}')
+ r_data = this_card.add_result(play, chances, secondary_play)
+ if r_data:
+ return r_data
+ else:
+ logging.warning(f'Could not find valid column trying new values')
+ for x in EXACT_CHANCES:
+ if x < math.floor(chances):
+ logging.warning(f'Using the whole number {math.floor(chances)}')
+ r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play)
+ if r_data:
+ return r_data
+ else:
+ logging.error(f'Whole number was no good')
+ break
+
+ if x < chances:
+ logging.warning(f'Trying {x} chances now')
+ r_data = this_card.add_result(play, x, secondary_play)
+ if r_data:
+ return r_data
+ else:
+ logging.warning(f'No good; checking the next value')
+
+ logging.error('Could not assign chances\n')
+ logging.debug(f'vl: {vl.sample_output()}')
+ logging.debug(f'vr: {vr.sample_output()}')
+ return 0, 0
+
+ def get_pullside_of():
+ if batting_card.hand == 'L':
+ return 'rf'
+ elif batting_card.hand == 'R':
+ return 'lf'
+ elif data.vs_hand == 'L':
+ return 'lf'
+ else:
+ return 'rf'
+
+ def get_preferred_mif(ratings):
+ if batting_card.hand == 'L' and ratings.slap_rate > .24:
+ return 'ss'
+ elif batting_card.hand == 'L' or (batting_card.hand == 'R' and ratings.slap_rate > .24):
+ return '2b'
+ else:
+ return 'ss'
+
+ new_battingratings = []
+ for card, data, new_ratings in [
+ (vl, copy.deepcopy(vl_ratings), BattingCardRatingsModel(
+ battingcard=vl_ratings.battingcard, pull_rate=vl_ratings.pull_rate, center_rate=vl_ratings.center_rate,
+ slap_rate=vl_ratings.slap_rate, vs_hand='L')),
+ (vr, copy.deepcopy(vr_ratings), BattingCardRatingsModel(
+ battingcard=vr_ratings.battingcard, pull_rate=vr_ratings.pull_rate, center_rate=vr_ratings.center_rate,
+ slap_rate=vr_ratings.slap_rate, vs_hand='R'))]:
+ logging.info(f'\n\nBeginning v{data.vs_hand}')
+ pull_of = get_pullside_of()
+ pref_mif = get_preferred_mif(data)
+
+ res_chances = data.bp_homerun
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(card, PLAY_RESULTS['bp-hr'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.bp_homerun += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.hbp
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.hbp += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.homerun
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_pull > 0:
+ data.double_pull += res_chances
+ elif data.double_two > 0:
+ data.double_two += res_chances
+ elif data.triple > 0:
+ data.triple += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.double_pull > (data.flyout_rf_b + data.flyout_lf_b) and data.double_pull > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-{pull_of}']
+ elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ elif data.double_pull > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-{pull_of}']
+ elif data.double_three > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do***']
+ elif data.double_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do**']
+ elif data.triple > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['tr']
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['hr'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.homerun += r_val[0]
+ if r_val[1] > 0:
+ if secondary.short_name[:4] == 'DO (':
+ data.double_pull -= r_val[1]
+ new_ratings.double_pull += r_val[1]
+ elif 'lf' in secondary.short_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.short_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif '***' in secondary.short_name:
+ data.double_three -= r_val[1]
+ new_ratings.double_three += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+ elif 'TR' in secondary.short_name:
+ data.triple -= r_val[1]
+ new_ratings.triple += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.triple
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_pull > 0:
+ data.double_pull += res_chances
+ elif data.double_two > 0:
+ data.double_two += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ elif data.double_pull > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-{pull_of}']
+ elif data.double_three > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do***']
+ elif data.double_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do**']
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['tr'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.triple += r_val[0]
+ if r_val[1] > 0:
+ if 'DO (' in secondary.short_name:
+ data.double_pull -= r_val[1]
+ new_ratings.double_pull += r_val[1]
+ elif 'lf' in secondary.short_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.short_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif '***' in secondary.short_name:
+ data.double_three -= r_val[1]
+ new_ratings.double_three += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.double_three
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_pull > 0:
+ data.double_pull += res_chances
+ elif data.double_two > 0:
+ data.double_two += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ elif data.double_pull > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-{pull_of}']
+ elif data.double_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do**']
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['do***'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.double_three += r_val[0]
+ if r_val[1] > 0:
+ if 'DO (' in secondary.short_name:
+ data.double_pull -= r_val[1]
+ new_ratings.double_pull += r_val[1]
+ elif 'lf' in secondary.short_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.short_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.double_pull
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_two > 0:
+ data.double_two += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B')
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly b')
+ elif data.single_one > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si*']
+ elif data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS[f'do-{pull_of}'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.double_pull += r_val[0]
+ if r_val[1] > 0:
+ if 'lf' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif '***' in secondary.short_name:
+ data.double_three -= r_val[1]
+ new_ratings.double_three += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.double_two
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ elif data.walk > 0:
+ data.walk += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ elif data.single_center > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si-cf']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['do**'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.double_two += r_val[0]
+ if r_val[1] > 0:
+ if 'lf' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.single_two
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ elif data.walk > 0:
+ data.walk += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.groundout_a > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A')
+ elif data.groundout_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B')
+ elif data.groundout_c > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C')
+ elif data.lineout > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})')
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['si**'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.single_two += r_val[0]
+ if r_val[1] > 0:
+ if 'C' in secondary.short_name:
+ data.groundout_c -= r_val[1]
+ new_ratings.groundout_c += r_val[1]
+ elif 'B' in secondary.short_name:
+ data.groundout_b -= r_val[1]
+ new_ratings.groundout_b += r_val[1]
+ elif 'A' in secondary.short_name:
+ data.groundout_a -= r_val[1]
+ new_ratings.groundout_a += r_val[1]
+ elif 'lo' in secondary.short_name:
+ data.lineout -= r_val[1]
+ new_ratings.lineout += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.single_center
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.single_one > 0:
+ data.single_one += res_chances
+ elif data.walk > 0:
+ data.walk += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.flyout_bq > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly B?', short_name=f'fly B?')
+ elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b:
+ secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B')
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (RF) B', short_name=f'fly B')
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B')
+ elif data.lineout > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})')
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['si-cf'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.single_center += r_val[0]
+ if r_val[1] > 0:
+ if '?' in secondary.short_name:
+ data.flyout_bq -= r_val[1]
+ new_ratings.flyout_bq += r_val[1]
+ elif 'LF' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'RF' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'lo' in secondary.short_name:
+ data.lineout -= r_val[1]
+ new_ratings.lineout += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.single_one
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.walk > 0:
+ data.walk += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.groundout_c > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C')
+ elif data.groundout_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B')
+ elif data.groundout_a > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A')
+ elif data.lineout > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})')
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['si*'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.single_one += r_val[0]
+ if r_val[1] > 0:
+ if 'C' in secondary.short_name:
+ data.groundout_c -= r_val[1]
+ new_ratings.groundout_c += r_val[1]
+ elif 'B' in secondary.short_name:
+ data.groundout_b -= r_val[1]
+ new_ratings.groundout_b += r_val[1]
+ elif 'A' in secondary.short_name:
+ data.groundout_a -= r_val[1]
+ new_ratings.groundout_a += r_val[1]
+ elif 'lo' in secondary.short_name:
+ data.lineout -= r_val[1]
+ new_ratings.lineout += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.walk
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ if data.strikeout > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'strikeout', short_name=f'so')
+ else:
+ secondary = None
+
+ r_val = assign_bchances(card, PLAY_RESULTS['walk'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.walk += r_val[0]
+ if r_val[1] > 0:
+ data.strikeout -= r_val[1]
+ new_ratings.strikeout += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.bp_single
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(card, PLAY_RESULTS['bp-si'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.bp_single += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ # Special lomax result
+ full_log(new_ratings, card)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'lo ({pref_mif}) max', short_name=f'lo ({pref_mif}) max'), Decimal(1))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ data.lineout -= r_val[0]
+ new_ratings.lineout += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.popout
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ this_if = '2b' if pref_mif == 'ss' else 'ss'
+ r_val = assign_bchances(
+ card,
+ PlayResult(full_name=f'popout ({this_if})', short_name=f'popout ({this_if})'),
+ Decimal(math.floor(ch))
+ )
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.lineout += res_chances
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.popout += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.flyout_a
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'fly (cf) A', short_name=f'fly (cf) A'), Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.strikeout += res_chances if data.strikeout > 2 else 0
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.flyout_a += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.flyout_lf_b
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.strikeout += res_chances if data.strikeout > 2 else 0
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.flyout_lf_b += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.flyout_rf_b
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.strikeout += res_chances if data.strikeout > 2 else 0
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.flyout_rf_b += r_val[0]
+
+ count_gb = 0
+
+ def get_gb_if():
+ if count_gb % 4 == 1:
+ return pref_mif
+ elif count_gb % 4 == 2:
+ return '2b' if pref_mif == 'ss' else 'ss'
+ elif count_gb % 4 == 3:
+ return '1b' if pref_mif == '2b' else 'p'
+ else:
+ return '3b' if pref_mif == 'ss' else 'p'
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.groundout_a
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ count_gb += 1
+ this_if = get_gb_if()
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A'),
+ Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.groundout_b += res_chances
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.groundout_a += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.groundout_b
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ count_gb += 1
+ this_if = get_gb_if()
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'gb ({this_if}) B', short_name=f'gb ({this_if}) B'),
+ Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.groundout_c += res_chances
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.groundout_b += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.groundout_c
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ count_gb += 1
+ this_if = get_gb_if()
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C'),
+ Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ data.strikeout += res_chances
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.groundout_c += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.lineout
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ this_if = '3b' if pref_mif == 'ss' else '1b'
+ r_val = assign_bchances(
+ card,
+ PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})'),
+ Decimal(math.floor(ch))
+ )
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.lineout += r_val[0]
+
+ full_log(new_ratings, card)
+ retries = 0
+ res_chances = data.strikeout
+ while res_chances >= 1:
+ if res_chances < 1 or retries > 0:
+ break
+
+ ch = get_chances(res_chances)
+ r_val = assign_bchances(
+ card, PlayResult(full_name=f'strikeout', short_name=f'strikeout'), Decimal(math.floor(ch)))
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if r_val[0] == 0:
+ break
+ else:
+ res_chances -= r_val[0]
+ new_ratings.strikeout += r_val[0]
+
+ log_data = vl.sample_output() if data.vs_hand == 'L' else vr.sample_output()
+ logging.info(f'Pre-filler total chances: {new_ratings.total_chances()}\n{log_data}')
+ plays = sorted(
+ [(data.strikeout, 'so'), (data.lineout, 'lo'), (data.groundout_c, 'gb'), (data.popout, 'po')],
+ key=lambda z: z[0],
+ reverse=True
+ )
+ count_filler = -1
+ while not card.is_complete():
+ count_filler += 1
+ this_play = plays[count_filler % 4]
+ if this_play[1] == 'so':
+ play_res = PlayResult(full_name=f'strikeout', short_name=f'strikeout')
+ elif this_play[1] == 'lo':
+ this_if = '3b' if pref_mif == 'ss' else '1b'
+ play_res = PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})')
+ elif this_play[1] == 'gb':
+ count_gb += 1
+ this_if = get_gb_if()
+ play_res = PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C')
+ else:
+ play_res = PlayResult(full_name=f'popout (c)', short_name=f'popout (c)')
+
+ logging.info(f'Send Card Fill\n{play_res}')
+ r_val = card.card_fill(play_res)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if this_play[1] == 'so':
+ new_ratings.strikeout += r_val[0]
+ elif this_play[1] == 'lo':
+ new_ratings.lineout += r_val[0]
+ elif this_play[1] == 'gb':
+ new_ratings.groundout_c += r_val[0]
+ else:
+ new_ratings.popout += r_val[0]
+
+ full_log(new_ratings, card)
+
+ new_ratings.update_slash_lines()
+ new_battingratings.append(new_ratings)
+
+ vl_output = vl.card_output()
+ vr_output = vr.card_output()
+
+ logging.info(f'vl: {vl.sample_output()}')
+ logging.info(f'vr: {vr.sample_output()}')
+
+ vl_total = new_battingratings[0].total_chances()
+ vr_total = new_battingratings[1].total_chances()
+ logging.info(f'New Ratings\nTotal Chances:\n{vl_total}\n{new_battingratings[0]}\n\n'
+ f'Total Chances: {vr_total}\n{new_battingratings[1]}')
+
+ if vl_total + vr_total != Decimal(216):
+ raise ValueError(f'vl chances: {vl_total} / vr chances: {vr_total}')
+
+ pos_data = get_pos_data(positions)
+ return {
+ 'player': player,
+ 'card_type': 'batter',
+ # 'vl_one_2d6': '2-',
+ # 'vl_one_results': 'HOMERUN',
+ # 'vl_one_d20': ' ',
+ # 'vl_two_2d6': '2-',
+ # 'vl_two_results': 'fly (cf) B',
+ # 'vl_two_d20': '',
+ # 'vl_three_2d6': '2-
',
+ # 'vl_three_results': 'HR
fly (cf) B',
+ # 'vl_three_d20': '1-16
17-20',
+ # 'results_vr_one': 'Light Dongs',
+ # 'results_vr_two': 'Hefty Dongs',
+ # 'results_vr_three': 'Obese Dongs',
+ 'vl_one_2d6': vl_output['one_2d6'],
+ 'vl_one_results': vl_output['one_results'],
+ 'vl_one_d20': vl_output['one_d20'],
+ 'vl_two_2d6': vl_output['two_2d6'],
+ 'vl_two_results': vl_output['two_results'],
+ 'vl_two_d20': vl_output['two_d20'],
+ 'vl_three_2d6': vl_output['three_2d6'],
+ 'vl_three_results': vl_output['three_results'],
+ 'vl_three_d20': vl_output['three_d20'],
+ 'vr_one_2d6': vr_output['one_2d6'],
+ 'vr_one_results': vr_output['one_results'],
+ 'vr_one_d20': vr_output['one_d20'],
+ 'vr_two_2d6': vr_output['two_2d6'],
+ 'vr_two_results': vr_output['two_results'],
+ 'vr_two_d20': vr_output['two_d20'],
+ 'vr_three_2d6': vr_output['three_2d6'],
+ 'vr_three_results': vr_output['three_results'],
+ 'vr_three_d20': vr_output['three_d20'],
+ 'hand': batting_card.hand,
+ 'bat_card': batting_card,
+ 'stealing_string': steal_string,
+ 'rarity_file': rarity_file,
+ 'new_ratings_vl': new_battingratings[0],
+ 'new_ratings_vr': new_battingratings[1],
+ 'position_string': pos_data['string'],
+ 'position_font': pos_data['font'],
+ 'position_margin': pos_data['margin']
+ }
+
+
+def get_pitcher_card_data(player, pitching_card, ratings_vl, ratings_vr, positions) -> dict:
+ player_binary = pitching_card.player_id % 2
+ logging.info(f'\n\nRunning Card for {player.p_name}')
+ rarity_file = encoded_images[player.rarity.name]
+
+ vl_dict = model_to_dict(ratings_vl)
+ vl_dict['pitchingcard'] = ratings_vl.pitchingcard_id
+ vl_ratings = PitchingCardRatingsModel(**vl_dict)
+ vl = FullPitchingCard(
+ ratings=vl_ratings,
+ offense_col=pitching_card.offense_col,
+ alt_direction=player_binary
+ )
+ vr_dict = model_to_dict(ratings_vr)
+ vr_dict['pitchingcard'] = ratings_vr.pitchingcard_id
+ vr_ratings = PitchingCardRatingsModel(**vr_dict)
+ vr = FullPitchingCard(
+ ratings=vr_ratings,
+ offense_col=pitching_card.offense_col,
+ alt_direction=player_binary
+ )
+
+ def assign_pchances(
+ this_card: FullPitchingCard, play: PlayResult, chances: Decimal,
+ secondary_play: Optional[PlayResult] = None):
+ logging.info(f'Assign pitching chances\n{play}\nChances: {chances}\nBackup: {secondary_play}')
+ r_data = this_card.add_result(play, chances, secondary_play)
+ if r_data:
+ return r_data
+ else:
+ logging.warning(f'Could not find valid column trying new values')
+ for x in EXACT_CHANCES + [Decimal('0.95')]:
+ if x < math.floor(chances - Decimal('0.05')):
+ logging.warning(f'Using the whole number {math.floor(chances)}')
+ r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play)
+ if r_data:
+ return r_data
+ else:
+ logging.error(f'Whole number was no good')
+ break
+
+ if x < chances and secondary_play is not None:
+ logging.warning(f'Trying {x} chances now')
+ r_data = this_card.add_result(play, x, secondary_play)
+ if r_data:
+ return r_data
+ else:
+ logging.warning(f'No good; checking the next value')
+
+ logging.error('Could not assign chances\n')
+ logging.debug(f'vl: {vl.sample_output()}')
+ logging.debug(f'vr: {vr.sample_output()}')
+ return 0, 0
+
+ def get_preferred_mif(ratings):
+ if pitching_card.hand == 'L' and ratings.vs_hand == 'L':
+ return 'ss'
+ elif pitching_card.hand == 'L' or (pitching_card.hand == 'R' and ratings.vs_hand == 'R'):
+ return '2b'
+ else:
+ return 'ss'
+
+ new_pitchingratings = []
+ for card, data, new_ratings in [
+ (vl, copy.deepcopy(vl_ratings), PitchingCardRatingsModel(
+ pitchingcard=vl_ratings.pitchingcard, vs_hand='L')),
+ (vr, copy.deepcopy(vr_ratings), PitchingCardRatingsModel(
+ pitchingcard=vr_ratings.pitchingcard, vs_hand='R'))]:
+ logging.info(f'\n\nBeginning v{data.vs_hand}')
+
+ res_chances = data.bp_homerun
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PLAY_RESULTS['bp-hr'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.bp_homerun += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.hbp
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.hbp += r_val[0]
+
+ if r_val[0] == 0:
+ break
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_p
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'gb (p) X', short_name=f'gb (p) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_p += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_c
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'catch X', short_name=f'catch X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_c += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_1b
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'gb (1b) X', short_name=f'gb (1b) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_1b += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_3b
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'gb (3b) X', short_name=f'gb (3b) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_3b += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_rf
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'fly (rf) X', short_name=f'fly (rf) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_rf += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_lf
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'fly (lf) X', short_name=f'fly (lf) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_lf += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_2b
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'gb (2b) X', short_name=f'gb (2b) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_2b += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_cf
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'fly (cf) X', short_name=f'fly (cf) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_cf += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.xcheck_ss
+ while res_chances > 0:
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'gb (ss) X', short_name=f'gb (ss) X'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.xcheck_ss += r_val[0]
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.walk
+ while res_chances >= 1:
+ ch = get_chances(res_chances)
+ if data.strikeout > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'strikeout', short_name=f'so')
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['walk'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.walk += r_val[0]
+ if r_val[1] > 0:
+ data.strikeout -= r_val[1]
+ new_ratings.strikeout += r_val[1]
+
+ if r_val[0] == 0:
+ break
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.homerun
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_cf > 0:
+ data.double_cf += res_chances
+ elif data.double_two > 0:
+ data.double_two += res_chances
+ elif data.triple > 0:
+ data.triple += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.double_cf > (data.flyout_rf_b + data.flyout_lf_b) and data.double_cf > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-cf']
+ elif data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-cf']
+ elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ elif data.double_cf > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-cf']
+ elif data.double_three > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do***']
+ elif data.double_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do**']
+ elif data.triple > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['tr']
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['hr'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.homerun += r_val[0]
+ if r_val[1] > 0:
+ if 'DO (' in secondary.short_name:
+ data.double_cf -= r_val[1]
+ new_ratings.double_cf += r_val[1]
+ elif 'lf' in secondary.short_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'cf' in secondary.short_name:
+ data.flyout_cf_b -= r_val[1]
+ new_ratings.flyout_cf_b += r_val[1]
+ elif 'rf' in secondary.short_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif '***' in secondary.short_name:
+ data.double_three -= r_val[1]
+ new_ratings.double_three += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+ elif 'TR' in secondary.short_name:
+ data.triple -= r_val[1]
+ new_ratings.triple += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.triple
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_cf > 0:
+ data.double_cf += res_chances
+ elif data.double_two > 0:
+ data.double_two += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ elif data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-cf']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ elif data.double_cf > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-cf']
+ elif data.double_three > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do***']
+ elif data.double_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do**']
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['tr'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.triple += r_val[0]
+ if r_val[1] > 0:
+ if 'DO (' in secondary.short_name:
+ data.double_cf -= r_val[1]
+ new_ratings.double_cf += r_val[1]
+ elif 'lf' in secondary.short_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'cf' in secondary.short_name:
+ data.flyout_cf_b -= r_val[1]
+ new_ratings.flyout_cf_b += r_val[1]
+ elif 'rf' in secondary.short_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif '***' in secondary.short_name:
+ data.double_three -= r_val[1]
+ new_ratings.double_three += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.double_three
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_cf > 0:
+ data.double_cf += res_chances
+ elif data.double_two > 0:
+ data.double_two += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ elif data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-cf']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ elif data.double_cf > max(1 - ch, 0):
+ secondary = PLAY_RESULTS[f'do-cf']
+ elif data.double_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['do**']
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['do***'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.double_three += r_val[0]
+ if r_val[1] > 0:
+ if 'DO (' in secondary.short_name:
+ data.double_cf -= r_val[1]
+ new_ratings.double_cf += r_val[1]
+ elif 'lf' in secondary.short_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'cf' in secondary.short_name:
+ data.flyout_cf_b -= r_val[1]
+ new_ratings.flyout_cf_b += r_val[1]
+ elif 'rf' in secondary.short_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.double_cf
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.double_two > 0:
+ data.double_two += res_chances
+ elif data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (cf) B', short_name=f'fly B')
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B')
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly b')
+ elif data.single_one > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si*']
+ elif data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS[f'do-cf'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.double_cf += r_val[0]
+ if r_val[1] > 0:
+ if 'lf' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'cf' in secondary.full_name:
+ data.flyout_cf_b -= r_val[1]
+ new_ratings.flyout_cf_b += r_val[1]
+ elif '***' in secondary.short_name:
+ data.double_three -= r_val[1]
+ new_ratings.double_three += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+ elif '**' in secondary.short_name:
+ data.double_two -= r_val[1]
+ new_ratings.double_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.double_two
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.single_two > 0:
+ data.single_two += res_chances
+ elif data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ elif data.walk > 0:
+ data.walk += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.single_two > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si**']
+ elif data.single_center > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['si-cf']
+ elif data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-cf']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['do**'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.double_two += r_val[0]
+ if r_val[1] > 0:
+ if 'lf' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'cf' in secondary.full_name:
+ data.flyout_cf_b -= r_val[1]
+ new_ratings.flyout_cf_b += r_val[1]
+ elif 'SI' in secondary.short_name:
+ data.single_two -= r_val[1]
+ new_ratings.single_two += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.single_two
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.single_center > 0:
+ data.single_center += res_chances
+ elif data.single_one > 0:
+ data.single_one += res_chances
+ elif data.walk > 0:
+ data.walk += res_chances
+ break
+
+ pref_mif = get_preferred_mif(new_ratings)
+ ch = get_chances(res_chances)
+ if data.groundout_a > max(1 - ch, 0):
+ temp_mif = get_preferred_mif(new_ratings)
+ pref_mif = 'ss' if temp_mif == '2b' else '2b'
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A')
+ elif data.groundout_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B')
+ elif data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-cf']
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-lf']
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PLAY_RESULTS['fly-rf']
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['si**'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.single_two += r_val[0]
+ if r_val[1] > 0:
+ if 'B' in secondary.short_name:
+ data.groundout_b -= r_val[1]
+ new_ratings.groundout_b += r_val[1]
+ elif 'A' in secondary.short_name:
+ data.groundout_a -= r_val[1]
+ new_ratings.groundout_a += r_val[1]
+ elif 'lf' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'rf' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+ elif 'cf' in secondary.full_name:
+ data.flyout_cf_b -= r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.single_center
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.single_one > 0:
+ data.single_one += res_chances
+ elif data.walk > 0:
+ data.walk += res_chances
+ break
+
+ ch = get_chances(res_chances)
+ if data.flyout_cf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (cf) B', short_name=f'fly B')
+ elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b:
+ secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B')
+ elif data.flyout_rf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly B')
+ elif data.flyout_lf_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B')
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['si-cf'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.single_center += r_val[0]
+ if r_val[1] > 0:
+ if 'CF' in secondary.short_name:
+ data.flyout_cf_b -= r_val[1]
+ new_ratings.flyout_cf_b += r_val[1]
+ elif 'LF' in secondary.full_name:
+ data.flyout_lf_b -= r_val[1]
+ new_ratings.flyout_lf_b += r_val[1]
+ elif 'RF' in secondary.full_name:
+ data.flyout_rf_b -= r_val[1]
+ new_ratings.flyout_rf_b += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.single_one
+ retries = 0
+ while res_chances > 0:
+ if res_chances < 1 or retries > 0:
+ if data.walk > 0:
+ data.walk += res_chances
+ break
+
+ pref_mif = get_preferred_mif(new_ratings)
+ ch = get_chances(res_chances)
+ if data.groundout_b > max(1 - ch, 0):
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B')
+ elif data.groundout_a > max(1 - ch, 0):
+ temp_mif = get_preferred_mif(new_ratings)
+ pref_mif = 'ss' if temp_mif == '2b' else '2b'
+ secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A')
+ else:
+ secondary = None
+
+ r_val = assign_pchances(card, PLAY_RESULTS['si*'], ch, secondary)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.single_one += r_val[0]
+ if r_val[1] > 0:
+ if 'B' in secondary.short_name:
+ data.groundout_b -= r_val[1]
+ new_ratings.groundout_b += r_val[1]
+ elif 'A' in secondary.short_name:
+ data.groundout_a -= r_val[1]
+ new_ratings.groundout_a += r_val[1]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.bp_single
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PLAY_RESULTS['bp-si'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.bp_single += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.strikeout
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PlayResult(full_name=f'strikeout', short_name=f'so'), ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.strikeout += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.flyout_cf_b
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PLAY_RESULTS['fly-cf'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.flyout_cf_b += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.flyout_lf_b
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PLAY_RESULTS['fly-lf'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.flyout_lf_b += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.flyout_rf_b
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(card, PLAY_RESULTS['fly-rf'], ch)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.flyout_rf_b += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.groundout_a
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+
+ temp_mif = get_preferred_mif(new_ratings)
+ pref_mif = 'ss' if temp_mif == '2b' else '2b'
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(
+ card,
+ PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A'),
+ ch
+ )
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.groundout_a += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ full_log(new_ratings, card, info=True)
+ res_chances = data.groundout_b
+ retries = 0
+ while res_chances > 0:
+ if retries > 0:
+ break
+
+ pref_mif = get_preferred_mif(new_ratings)
+ ch = get_chances(res_chances)
+ r_val = assign_pchances(
+ card,
+ PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B'),
+ ch
+ )
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ res_chances -= r_val[0]
+ new_ratings.groundout_b += r_val[0]
+
+ if r_val[0] == 0:
+ retries += 1
+
+ log_data = vl.sample_output() if data.vs_hand == 'L' else vr.sample_output()
+ logging.info(f'Pre-filler total chances: {new_ratings.total_chances()}\n{log_data}')
+ plays = sorted(
+ [(data.strikeout, 'so'), (data.groundout_a, 'gb'), (data.flyout_lf_b, 'lf'), (data.flyout_rf_b, 'rf')],
+ key=lambda z: z[0],
+ reverse=True
+ )
+ count_filler = -1
+ pref_mif = get_preferred_mif(new_ratings)
+ while not card.is_complete():
+ count_filler += 1
+ this_play = plays[count_filler % 4]
+ if this_play[1] == 'so':
+ play_res = PlayResult(full_name=f'strikeout', short_name=f'strikeout')
+ elif this_play[1] == 'gb':
+ this_if = '3b' if pref_mif == 'ss' else '1b'
+ play_res = PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A')
+ elif this_play[1] == 'lf':
+ play_res = PLAY_RESULTS['fly-lf']
+ else:
+ play_res = PLAY_RESULTS['fly-rf']
+
+ logging.info(f'Send Card Fill\n{play_res}')
+ r_val = card.card_fill(play_res)
+ logging.info(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
+
+ if this_play[1] == 'so':
+ new_ratings.strikeout += r_val[0]
+ elif this_play[1] == 'gb':
+ new_ratings.groundout_a += r_val[0]
+ elif this_play[1] == 'lf':
+ new_ratings.flyout_lf_b += r_val[0]
+ else:
+ new_ratings.flyout_rf_b += r_val[0]
+
+ full_log(new_ratings, card)
+
+ card.add_fatigue()
+ new_ratings.update_slash_lines()
+ new_pitchingratings.append(new_ratings)
+
+ vl_output = vl.card_output()
+ vr_output = vr.card_output()
+
+ logging.info(f'vl: {vl.sample_output()}')
+ logging.info(f'vr: {vr.sample_output()}')
+
+ vl_total = new_pitchingratings[0].total_chances()
+ vr_total = new_pitchingratings[1].total_chances()
+ logging.info(f'New Ratings\nTotal Chances:\n{vl_total}\n{new_pitchingratings[0]}\n\n'
+ f'Total Chances: {vr_total}\n{new_pitchingratings[1]}')
+
+ pos_data = get_pos_data(positions, is_pitcher=True)
+ return {
+ 'player': player,
+ 'card_type': 'pitcher',
+ # 'vl_one_2d6': '2-',
+ # 'vl_one_results': 'HOMERUN',
+ # 'vl_one_d20': ' ',
+ # 'vl_two_2d6': '2-',
+ # 'vl_two_results': 'fly (cf) B',
+ # 'vl_two_d20': '',
+ # 'vl_three_2d6': '2-
',
+ # 'vl_three_results': 'HR
fly (cf) B',
+ # 'vl_three_d20': '1-16
17-20',
+ # 'results_vr_one': 'Light Dongs',
+ # 'results_vr_two': 'Hefty Dongs',
+ # 'results_vr_three': 'Obese Dongs',
+ 'vl_one_2d6': vl_output['one_2d6'],
+ 'vl_one_results': vl_output['one_results'],
+ 'vl_one_d20': vl_output['one_d20'],
+ 'vl_two_2d6': vl_output['two_2d6'],
+ 'vl_two_results': vl_output['two_results'],
+ 'vl_two_d20': vl_output['two_d20'],
+ 'vl_three_2d6': vl_output['three_2d6'],
+ 'vl_three_results': vl_output['three_results'],
+ 'vl_three_d20': vl_output['three_d20'],
+ 'vr_one_2d6': vr_output['one_2d6'],
+ 'vr_one_results': vr_output['one_results'],
+ 'vr_one_d20': vr_output['one_d20'],
+ 'vr_two_2d6': vr_output['two_2d6'],
+ 'vr_two_results': vr_output['two_results'],
+ 'vr_two_d20': vr_output['two_d20'],
+ 'vr_three_2d6': vr_output['three_2d6'],
+ 'vr_three_results': vr_output['three_results'],
+ 'vr_three_d20': vr_output['three_d20'],
+ 'hand': pitching_card.hand,
+ 'pit_card': pitching_card,
+ 'rarity_file': rarity_file,
+ 'position_string': pos_data['string'],
+ 'position_font': pos_data['font'],
+ 'position_margin': pos_data['margin'],
+ 'new_ratings_vl': new_pitchingratings[0],
+ 'new_ratings_vr': new_pitchingratings[1],
+ }
diff --git a/app/db_engine.py b/app/db_engine.py
new file mode 100644
index 0000000..c5c283e
--- /dev/null
+++ b/app/db_engine.py
@@ -0,0 +1,1005 @@
+import math
+from datetime import datetime
+from typing import List
+import logging
+import os
+
+from pandas import DataFrame
+from peewee import *
+from peewee import ModelSelect
+from playhouse.shortcuts import model_to_dict
+
+db = SqliteDatabase(
+ 'storage/pd_master.db',
+ pragmas={
+ 'journal_mode': 'wal',
+ 'cache_size': -1 * 64000,
+ 'synchronous': 0
+ }
+)
+
+date = f'{datetime.now().year}-{datetime.now().month}-{datetime.now().day}'
+log_level = logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN'
+logging.basicConfig(
+ filename=f'logs/database/{date}.log',
+ format='%(asctime)s - database - %(levelname)s - %(message)s',
+ level=log_level
+)
+
+CARDSETS = {
+ 'ranked': {
+ 'primary': [9, 10, 13] # 2023, 23 Promos, 2018
+ },
+ 'minor-league': {
+ 'primary': [13, 8], # 2018, Mario
+ 'secondary': [9, 3] # 2023, 2022
+ },
+ 'major-league': {
+ 'primary': [13, 9, 8, 6], # 2018, 2023, Mario, 2013
+ 'secondary': [3, 12] # 2022, 2008
+ },
+ 'hall-of-fame': {
+ 'primary': [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
+ },
+ 'tens': {
+ 'primary': [11, 7, 6, 12], # 2016, 2012, 2013, 2008, Mario
+ 'secondary': [13, 5] # 2018, 2019
+ },
+ 'gauntlet-3': {
+ 'primary': [13, 9], # 2018, 2023
+ 'secondary': [5, 11] # 2019, 2016
+ }
+}
+
+
+def model_csv_headers(this_obj, exclude=None) -> List:
+ data = model_to_dict(this_obj, recurse=False, exclude=exclude)
+ return [x for x in data.keys()]
+
+
+def model_to_csv(this_obj, exclude=None) -> List:
+ data = model_to_dict(this_obj, recurse=False, exclude=exclude)
+ return [x for x in data.values()]
+
+
+def query_to_csv(all_items: ModelSelect, exclude=None):
+ if all_items.count() == 0:
+ data_list = [['No data found']]
+ else:
+ data_list = [model_csv_headers(all_items[0], exclude=exclude)]
+ for x in all_items:
+ data_list.append(model_to_csv(x, exclude=exclude))
+
+ return DataFrame(data_list).to_csv(header=False, index=False)
+
+
+def complex_data_to_csv(complex_data: List):
+ if len(complex_data) == 0:
+ data_list = [['No data found']]
+ else:
+ data_list = [[x for x in complex_data[0].keys()]]
+ for line in complex_data:
+ logging.debug(f'line: {line}')
+ this_row = []
+ for key in line:
+ logging.debug(f'key: {key}')
+ if line[key] is None:
+ this_row.append('')
+
+ elif isinstance(line[key], dict):
+ if 'name' in line[key]:
+ this_row.append(line[key]['name'])
+ elif 'abbrev' in line[key]:
+ this_row.append(line[key]['abbrev'])
+ else:
+ this_row.append(line[key]['id'])
+
+ elif isinstance(line[key], int) and line[key] > 100000000:
+ this_row.append(f"'{line[key]}")
+
+ elif isinstance(line[key], str) and ',' in line[key]:
+ this_row.append(line[key].replace(",", "-_-"))
+
+ else:
+ this_row.append(line[key])
+
+ data_list.append(this_row)
+
+ return DataFrame(data_list).to_csv(header=False, index=False)
+
+
+class BaseModel(Model):
+ class Meta:
+ database = db
+
+
+class Current(BaseModel):
+ season = IntegerField()
+ week = IntegerField(default=0)
+ gsheet_template = CharField()
+ gsheet_version = CharField()
+ live_scoreboard = IntegerField()
+
+ @staticmethod
+ def latest():
+ latest_current = Current.select().order_by(-Current.id).get()
+ return latest_current
+
+
+db.create_tables([Current])
+
+
+class Rarity(BaseModel):
+ value = IntegerField()
+ name = CharField(unique=True)
+ color = CharField()
+
+ def __str__(self):
+ return self.name
+
+
+db.create_tables([Rarity])
+
+
+class Event(BaseModel):
+ name = CharField()
+ short_desc = CharField(null=True)
+ url = CharField(null=True)
+ long_desc = CharField(null=True)
+ thumbnail = CharField(null=True)
+ active = BooleanField(default=False)
+
+
+db.create_tables([Event])
+
+
+class Cardset(BaseModel):
+ name = CharField()
+ description = CharField()
+ event = ForeignKeyField(Event, null=True)
+ for_purchase = BooleanField(default=True) # for_purchase
+ total_cards = IntegerField()
+ in_packs = BooleanField(default=True)
+ ranked_legal = BooleanField(default=True)
+
+ def __str__(self):
+ return self.name
+
+
+db.create_tables([Cardset])
+
+
+class MlbPlayer(BaseModel):
+ first_name = CharField()
+ last_name = CharField()
+ key_fangraphs = IntegerField(null=True)
+ key_bbref = CharField(null=True)
+ key_retro = CharField(null=True)
+ key_mlbam = IntegerField(null=True)
+ offense_col = IntegerField(default=1)
+
+
+db.create_tables([MlbPlayer])
+
+
+class Player(BaseModel):
+ player_id = IntegerField(primary_key=True)
+ p_name = CharField()
+ cost = IntegerField(default=0)
+ image = CharField()
+ image2 = CharField(null=True)
+ mlbclub = CharField()
+ franchise = CharField()
+ cardset = ForeignKeyField(Cardset)
+ set_num = IntegerField()
+ rarity = ForeignKeyField(Rarity)
+ pos_1 = CharField()
+ pos_2 = CharField(null=True)
+ pos_3 = CharField(null=True)
+ pos_4 = CharField(null=True)
+ pos_5 = CharField(null=True)
+ pos_6 = CharField(null=True)
+ pos_7 = CharField(null=True)
+ pos_8 = CharField(null=True)
+ headshot = CharField(null=True)
+ vanity_card = CharField(null=True)
+ strat_code = CharField(null=True)
+ bbref_id = CharField(null=True)
+ fangr_id = CharField(null=True)
+ description = CharField()
+ quantity = IntegerField(default=999)
+ mlbplayer = ForeignKeyField(MlbPlayer, null=True)
+
+ def __str__(self):
+ return f'{self.cardset} {self.p_name} ({self.rarity.name})'
+
+ # def __eq__(self, other):
+ # if self.cardset.id == other.cardset.id and self.name == other.name:
+ # return True
+ # else:
+ # return False
+
+ def __lt__(self, other):
+ if self.wara < other.wara:
+ return True
+ elif self.wara > other.wara:
+ return False
+ elif self.name < other.name:
+ return True
+ else:
+ return False
+
+ def get_all_pos(self):
+ all_pos = []
+
+ if self.pos_1 and self.pos_1 != 'CP':
+ all_pos.append(self.pos_1)
+ if self.pos_2 and self.pos_2 != 'CP':
+ all_pos.append(self.pos_2)
+ if self.pos_3 and self.pos_3 != 'CP':
+ all_pos.append(self.pos_3)
+ if self.pos_4 and self.pos_4 != 'CP':
+ all_pos.append(self.pos_4)
+ if self.pos_5 and self.pos_5 != 'CP':
+ all_pos.append(self.pos_5)
+ if self.pos_6 and self.pos_6 != 'CP':
+ all_pos.append(self.pos_6)
+ if self.pos_7 and self.pos_7 != 'CP':
+ all_pos.append(self.pos_7)
+ if self.pos_8 and self.pos_8 != 'CP':
+ all_pos.append(self.pos_8)
+
+ return all_pos
+
+ def change_on_sell(self):
+ # caps = {
+ # 'replacement': 15,
+ # 'reserve': 50,
+ # 'starter': 200,
+ # 'all-star': 750,
+ # 'mvp': 2500,
+ # 'hof': 999999999
+ # }
+ logging.info(f'{self.p_name} cost changing from: {self.cost}')
+ self.cost = max(math.floor(self.cost * .95), 1)
+ # if self.quantity != 999:
+ # self.quantity += 1
+ logging.info(f'{self.p_name} cost now: {self.cost}')
+ self.save()
+
+ def change_on_buy(self):
+ logging.info(f'{self.p_name} cost changing from: {self.cost}')
+ self.cost = math.ceil(self.cost * 1.1)
+ # if self.quantity != 999:
+ # self.quantity -= 1
+ logging.info(f'{self.p_name} cost now: {self.cost}')
+ self.save()
+
+
+db.create_tables([Player])
+
+
+class Team(BaseModel):
+ abbrev = CharField()
+ sname = CharField()
+ lname = CharField()
+ gmid = IntegerField()
+ gmname = CharField()
+ gsheet = CharField()
+ wallet = IntegerField()
+ team_value = IntegerField()
+ collection_value = IntegerField()
+ logo = CharField(null=True)
+ color = CharField(null=True)
+ season = IntegerField()
+ event = ForeignKeyField(Event, null=True)
+ career = IntegerField(default=0)
+ ranking = IntegerField(default=1000)
+ has_guide = BooleanField(default=False)
+ is_ai = IntegerField(null=True)
+
+ def __str__(self):
+ return f'S{self.season} {self.lname}'
+
+ @staticmethod
+ def get_by_owner(gmid, season=None):
+ if not season:
+ season = Current.get().season
+ team = Team.get_or_none((Team.gmid == gmid) & (Team.season == season))
+
+ if not team:
+ return None
+
+ return team
+
+ @staticmethod
+ def select_season(season=None):
+ if not season:
+ season = Current.get().season
+ return Team.select().where(Team.season == season)
+
+ @staticmethod
+ def get_season(abbrev, season=None):
+ if not season:
+ season = Current.get().season
+ return Team.get_or_none(Team.season == season, Team.abbrev == abbrev.upper())
+
+ def team_hash(self):
+ hash_string = f'{self.sname[-1]}{self.gmid / 6950123:.0f}{self.sname[-2]}{self.gmid / 42069123:.0f}'
+ logging.info(f'string: {hash_string}')
+ return hash_string
+
+
+db.create_tables([Team])
+
+
+class PackType(BaseModel):
+ name = CharField()
+ card_count = IntegerField()
+ description = CharField()
+ cost = IntegerField()
+ available = BooleanField(default=True)
+
+
+db.create_tables([PackType])
+
+
+class Pack(BaseModel):
+ team = ForeignKeyField(Team)
+ pack_type = ForeignKeyField(PackType)
+ pack_team = ForeignKeyField(Team, null=True)
+ pack_cardset = ForeignKeyField(Cardset, null=True)
+ open_time = DateTimeField(null=True)
+
+
+db.create_tables([Pack])
+
+
+class Card(BaseModel):
+ player = ForeignKeyField(Player, null=True)
+ team = ForeignKeyField(Team, null=True)
+ pack = ForeignKeyField(Pack, null=True)
+ value = IntegerField(default=0)
+
+ def __str__(self):
+ if self.player:
+ return f'{self.player} - {self.team.sname}'
+ else:
+ return f'Blank - {self.team.sname}'
+
+ @staticmethod
+ def select_season(season):
+ return Card.select().join(Team).where(Card.team.season == season)
+
+
+db.create_tables([Card])
+
+
+class Roster(BaseModel):
+ team = ForeignKeyField(Team)
+ name = CharField()
+ roster_num = IntegerField()
+ card_1 = ForeignKeyField(Card)
+ card_2 = ForeignKeyField(Card)
+ card_3 = ForeignKeyField(Card)
+ card_4 = ForeignKeyField(Card)
+ card_5 = ForeignKeyField(Card)
+ card_6 = ForeignKeyField(Card)
+ card_7 = ForeignKeyField(Card)
+ card_8 = ForeignKeyField(Card)
+ card_9 = ForeignKeyField(Card)
+ card_10 = ForeignKeyField(Card)
+ card_11 = ForeignKeyField(Card)
+ card_12 = ForeignKeyField(Card)
+ card_13 = ForeignKeyField(Card)
+ card_14 = ForeignKeyField(Card)
+ card_15 = ForeignKeyField(Card)
+ card_16 = ForeignKeyField(Card)
+ card_17 = ForeignKeyField(Card)
+ card_18 = ForeignKeyField(Card)
+ card_19 = ForeignKeyField(Card)
+ card_20 = ForeignKeyField(Card)
+ card_21 = ForeignKeyField(Card)
+ card_22 = ForeignKeyField(Card)
+ card_23 = ForeignKeyField(Card)
+ card_24 = ForeignKeyField(Card)
+ card_25 = ForeignKeyField(Card)
+ card_26 = ForeignKeyField(Card)
+
+ def __str__(self):
+ return f'{self.team} Roster'
+
+ # def get_cards(self, team):
+ # all_cards = Card.select().where(Card.roster == self)
+ # this_roster = []
+ # return [this_roster.card1, this_roster.card2, this_roster.card3, this_roster.card4, this_roster.card5,
+ # this_roster.card6, this_roster.card7, this_roster.card8, this_roster.card9, this_roster.card10,
+ # this_roster.card11, this_roster.card12, this_roster.card13, this_roster.card14, this_roster.card15,
+ # this_roster.card16, this_roster.card17, this_roster.card18, this_roster.card19, this_roster.card20,
+ # this_roster.card21, this_roster.card22, this_roster.card23, this_roster.card24, this_roster.card25,
+ # this_roster.card26]
+
+
+class Result(BaseModel):
+ away_team = ForeignKeyField(Team)
+ home_team = ForeignKeyField(Team)
+ away_score = IntegerField()
+ home_score = IntegerField()
+ away_team_value = IntegerField(null=True)
+ home_team_value = IntegerField(null=True)
+ away_team_ranking = IntegerField(null=True)
+ home_team_ranking = IntegerField(null=True)
+ scorecard = CharField()
+ week = IntegerField()
+ season = IntegerField()
+ ranked = BooleanField()
+ short_game = BooleanField()
+ game_type = CharField(null=True)
+
+ @staticmethod
+ def select_season(season=None):
+ if not season:
+ season = Current.get().season
+ return Result.select().where(Result.season == season)
+
+
+class BattingStat(BaseModel):
+ card = ForeignKeyField(Card)
+ team = ForeignKeyField(Team)
+ roster_num = IntegerField()
+ vs_team = ForeignKeyField(Team)
+ result = ForeignKeyField(Result, null=True)
+ pos = CharField()
+ pa = IntegerField()
+ ab = IntegerField()
+ run = IntegerField()
+ hit = IntegerField()
+ rbi = IntegerField()
+ double = IntegerField()
+ triple = IntegerField()
+ hr = IntegerField()
+ bb = IntegerField()
+ so = IntegerField()
+ hbp = IntegerField()
+ sac = IntegerField()
+ ibb = IntegerField()
+ gidp = IntegerField()
+ sb = IntegerField()
+ cs = IntegerField()
+ bphr = IntegerField()
+ bpfo = IntegerField()
+ bp1b = IntegerField()
+ bplo = IntegerField()
+ xch = IntegerField()
+ xhit = IntegerField()
+ error = IntegerField()
+ pb = IntegerField()
+ sbc = IntegerField()
+ csc = IntegerField()
+ week = IntegerField()
+ season = IntegerField()
+ created = DateTimeField()
+ game_id = IntegerField()
+
+
+class PitchingStat(BaseModel):
+ card = ForeignKeyField(Card)
+ team = ForeignKeyField(Team)
+ roster_num = IntegerField()
+ vs_team = ForeignKeyField(Team)
+ result = ForeignKeyField(Result, null=True)
+ ip = FloatField()
+ hit = IntegerField()
+ run = IntegerField()
+ erun = IntegerField()
+ so = IntegerField()
+ bb = IntegerField()
+ hbp = IntegerField()
+ wp = IntegerField()
+ balk = IntegerField()
+ hr = IntegerField()
+ ir = IntegerField()
+ irs = IntegerField()
+ gs = IntegerField()
+ win = IntegerField()
+ loss = IntegerField()
+ hold = IntegerField()
+ sv = IntegerField()
+ bsv = IntegerField()
+ week = IntegerField()
+ season = IntegerField()
+ created = DateTimeField()
+ game_id = IntegerField()
+
+
+class Award(BaseModel):
+ name = CharField()
+ season = IntegerField()
+ timing = CharField(default="In-Season")
+ card = ForeignKeyField(Card, null=True)
+ team = ForeignKeyField(Team, null=True)
+ image = CharField(null=True)
+
+
+class Paperdex(BaseModel):
+ team = ForeignKeyField(Team)
+ player = ForeignKeyField(Player)
+ created = DateTimeField(default=int(datetime.timestamp(datetime.now())*1000))
+
+ # def add_to_paperdex(self, team, cards: list):
+ # for x in players:
+ # if not isinstance(x, Card):
+ # raise TypeError(f'The Pokedex can only take a list of Player or Card objects')
+ #
+ # Paperdex.get_or_create(team=team, player=player)
+
+
+class Reward(BaseModel):
+ name = CharField(null=True)
+ season = IntegerField()
+ week = IntegerField()
+ team = ForeignKeyField(Team)
+ created = DateTimeField()
+
+
+class GameRewards(BaseModel):
+ name = CharField()
+ pack_type = ForeignKeyField(PackType, null=True)
+ player = ForeignKeyField(Player, null=True)
+ money = IntegerField(null=True)
+
+
+class Notification(BaseModel):
+ created = DateTimeField()
+ title = CharField()
+ desc = CharField(null=True)
+ field_name = CharField()
+ message = CharField()
+ about = CharField() # f'{Topic}-{Object ID}'
+ ack = BooleanField(default=False)
+
+
+class GauntletReward(BaseModel):
+ name = CharField()
+ gauntlet = ForeignKeyField(Event)
+ reward = ForeignKeyField(GameRewards)
+ win_num = IntegerField()
+ loss_max = IntegerField(default=1)
+
+
+class GauntletRun(BaseModel):
+ team = ForeignKeyField(Team)
+ gauntlet = ForeignKeyField(Event)
+ wins = IntegerField(default=0)
+ losses = IntegerField(default=0)
+ gsheet = CharField(null=True)
+ created = DateTimeField(default=int(datetime.timestamp(datetime.now())*1000))
+ ended = DateTimeField(default=0)
+
+
+db.create_tables([
+ Roster, BattingStat, PitchingStat, Result, Award, Paperdex, Reward, GameRewards, Notification, GauntletReward,
+ GauntletRun
+])
+
+
+class BattingCard(BaseModel):
+ player = ForeignKeyField(Player)
+ variant = IntegerField()
+ steal_low = IntegerField()
+ steal_high = IntegerField()
+ steal_auto = BooleanField()
+ steal_jump = FloatField()
+ bunting = CharField()
+ hit_and_run = CharField()
+ running = IntegerField()
+ offense_col = IntegerField()
+ hand = CharField(default='R')
+
+
+bc_index = ModelIndex(BattingCard, (BattingCard.player, BattingCard.variant), unique=True)
+BattingCard.add_index(bc_index)
+
+
+class BattingCardRatings(BaseModel):
+ battingcard = ForeignKeyField(BattingCard)
+ vs_hand = CharField(default='R')
+ pull_rate = FloatField()
+ center_rate = FloatField()
+ slap_rate = FloatField()
+ homerun = FloatField()
+ bp_homerun = FloatField()
+ triple = FloatField()
+ double_three = FloatField()
+ double_two = FloatField()
+ double_pull = FloatField()
+ single_two = FloatField()
+ single_one = FloatField()
+ single_center = FloatField()
+ bp_single = FloatField()
+ hbp = FloatField()
+ walk = FloatField()
+ strikeout = FloatField()
+ lineout = FloatField()
+ popout = FloatField()
+ flyout_a = FloatField()
+ flyout_bq = FloatField()
+ flyout_lf_b = FloatField()
+ flyout_rf_b = FloatField()
+ groundout_a = FloatField()
+ groundout_b = FloatField()
+ groundout_c = FloatField()
+ avg = FloatField(null=True)
+ obp = FloatField(null=True)
+ slg = FloatField(null=True)
+
+
+bcr_index = ModelIndex(
+ BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True
+)
+BattingCardRatings.add_index(bcr_index)
+
+
+class PitchingCard(BaseModel):
+ player = ForeignKeyField(Player)
+ variant = IntegerField()
+ balk = IntegerField()
+ wild_pitch = IntegerField()
+ hold = IntegerField()
+ starter_rating = IntegerField()
+ relief_rating = IntegerField()
+ closer_rating = IntegerField(null=True)
+ batting = CharField(null=True)
+ offense_col = IntegerField()
+ hand = CharField(default='R')
+
+
+pc_index = ModelIndex(PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True)
+PitchingCard.add_index(pc_index)
+
+
+class PitchingCardRatings(BaseModel):
+ pitchingcard = ForeignKeyField(PitchingCard)
+ vs_hand = CharField(default='R')
+ homerun = FloatField()
+ bp_homerun = FloatField()
+ triple = FloatField()
+ double_three = FloatField()
+ double_two = FloatField()
+ double_cf = FloatField()
+ single_two = FloatField()
+ single_one = FloatField()
+ single_center = FloatField()
+ bp_single = FloatField()
+ hbp = FloatField()
+ walk = FloatField()
+ strikeout = FloatField()
+ flyout_lf_b = FloatField()
+ flyout_cf_b = FloatField()
+ flyout_rf_b = FloatField()
+ groundout_a = FloatField()
+ groundout_b = FloatField()
+ xcheck_p = FloatField()
+ xcheck_c = FloatField()
+ xcheck_1b = FloatField()
+ xcheck_2b = FloatField()
+ xcheck_3b = FloatField()
+ xcheck_ss = FloatField()
+ xcheck_lf = FloatField()
+ xcheck_cf = FloatField()
+ xcheck_rf = FloatField()
+ avg = FloatField(null=True)
+ obp = FloatField(null=True)
+ slg = FloatField(null=True)
+
+
+pcr_index = ModelIndex(
+ PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True
+)
+PitchingCardRatings.add_index(pcr_index)
+
+
+class CardPosition(BaseModel):
+ player = ForeignKeyField(Player)
+ variant = IntegerField()
+ position = CharField()
+ innings = IntegerField()
+ range = IntegerField()
+ error = IntegerField()
+ arm = IntegerField(null=True)
+ pb = IntegerField(null=True)
+ overthrow = IntegerField(null=True)
+
+
+pos_index = ModelIndex(
+ CardPosition, (CardPosition.player, CardPosition.variant, CardPosition.position), unique=True
+)
+CardPosition.add_index(pos_index)
+
+
+db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition])
+
+
+class StratGame(BaseModel):
+ season = IntegerField()
+ game_type = CharField()
+ away_team = ForeignKeyField(Team)
+ home_team = ForeignKeyField(Team)
+ week = IntegerField(default=1)
+ away_score = IntegerField(default=0)
+ home_score = IntegerField(default=0)
+ away_team_value = IntegerField(null=True)
+ home_team_value = IntegerField(null=True)
+ away_team_ranking = IntegerField(null=True)
+ home_team_ranking = IntegerField(null=True)
+ ranked = BooleanField(default=False)
+ short_game = BooleanField(default=False)
+ forfeit = BooleanField(default=False)
+
+
+class StratPlay(BaseModel):
+ game = ForeignKeyField(StratGame)
+ play_num = IntegerField()
+ batter = ForeignKeyField(Player, null=True)
+ batter_team = ForeignKeyField(Team, null=True)
+ pitcher = ForeignKeyField(Player)
+ pitcher_team = ForeignKeyField(Team)
+ on_base_code = CharField()
+ inning_half = CharField()
+ inning_num = IntegerField()
+ batting_order = IntegerField()
+ starting_outs = IntegerField()
+ away_score = IntegerField()
+ home_score = IntegerField()
+ batter_pos = CharField(null=True)
+
+ # These _final fields track the base this runner advances to post-play (None) if out
+ on_first = ForeignKeyField(Player, null=True)
+ on_first_final = IntegerField(null=True)
+ on_second = ForeignKeyField(Player, null=True)
+ on_second_final = IntegerField(null=True)
+ on_third = ForeignKeyField(Player, null=True)
+ on_third_final = IntegerField(null=True)
+ batter_final = IntegerField(null=True)
+
+ pa = IntegerField(default=0)
+ ab = IntegerField(default=0)
+ e_run = IntegerField(default=0)
+ run = IntegerField(default=0)
+ hit = IntegerField(default=0)
+ rbi = IntegerField(default=0)
+ double = IntegerField(default=0)
+ triple = IntegerField(default=0)
+ homerun = IntegerField(default=0)
+ bb = IntegerField(default=0)
+ so = IntegerField(default=0)
+ hbp = IntegerField(default=0)
+ sac = IntegerField(default=0)
+ ibb = IntegerField(default=0)
+ gidp = IntegerField(default=0)
+ bphr = IntegerField(default=0)
+ bpfo = IntegerField(default=0)
+ bp1b = IntegerField(default=0)
+ bplo = IntegerField(default=0)
+ sb = IntegerField(default=0)
+ cs = IntegerField(default=0)
+ outs = IntegerField(default=0)
+ wpa = FloatField(default=0.0)
+ re24 = FloatField(default=0.0)
+
+ # These fields are only required if the play is an x-check or baserunning play
+ catcher = ForeignKeyField(Player, null=True)
+ catcher_team = ForeignKeyField(Team, null=True)
+ defender = ForeignKeyField(Player, null=True)
+ defender_team = ForeignKeyField(Team, null=True)
+ runner = ForeignKeyField(Player, null=True)
+ runner_team = ForeignKeyField(Team, null=True)
+
+ check_pos = CharField(null=True)
+ error = IntegerField(default=0)
+ wild_pitch = IntegerField(default=0)
+ passed_ball = IntegerField(default=0)
+ pick_off = IntegerField(default=0)
+ balk = IntegerField(default=0)
+ is_go_ahead = BooleanField(default=False)
+ is_tied = BooleanField(default=False)
+ is_new_inning = BooleanField(default=False)
+
+
+class Decision(BaseModel):
+ season = IntegerField()
+ game = ForeignKeyField(StratGame)
+ pitcher = ForeignKeyField(Player)
+ pitcher_team = ForeignKeyField(Team)
+ week = IntegerField(default=1)
+ win = IntegerField(default=0)
+ loss = IntegerField(default=0)
+ hold = IntegerField(default=0)
+ is_save = IntegerField(default=0)
+ b_save = IntegerField(default=0)
+ irunners = IntegerField(default=0)
+ irunners_scored = IntegerField(default=0)
+ rest_ip = FloatField(default=0.0)
+ rest_required = IntegerField(default=0)
+ is_start = BooleanField(default=False)
+
+
+db.create_tables([StratGame, StratPlay, Decision])
+
+
+db.close()
+
+# scout_db = SqliteDatabase(
+# 'storage/card_creation.db',
+# pragmas={
+# 'journal_mode': 'wal',
+# 'cache_size': -1 * 64000,
+# 'synchronous': 0
+# }
+# )
+#
+#
+# class BaseModelScout(Model):
+# class Meta:
+# database = scout_db
+#
+#
+# class ScoutCardset(BaseModelScout):
+# set_title = CharField()
+# set_subtitle = CharField(null=True)
+#
+#
+# class ScoutPlayer(BaseModelScout):
+# sba_id = IntegerField(primary_key=True)
+# name = CharField()
+# fg_id = IntegerField()
+# br_id = CharField()
+# offense_col = IntegerField()
+# hand = CharField(default='R')
+#
+#
+# scout_db.create_tables([ScoutCardset, ScoutPlayer])
+#
+#
+# class BatterRatings(BaseModelScout):
+# id = CharField(unique=True, primary_key=True)
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# vs_hand = FloatField()
+# is_prep = BooleanField()
+# homerun = FloatField()
+# bp_homerun = FloatField()
+# triple = FloatField()
+# double_three = FloatField()
+# double_two = FloatField()
+# double_pull = FloatField()
+# single_two = FloatField()
+# single_one = FloatField()
+# single_center = FloatField()
+# bp_single = FloatField()
+# hbp = FloatField()
+# walk = FloatField()
+# strikeout = FloatField()
+# lineout = FloatField()
+# popout = FloatField()
+# flyout_a = FloatField()
+# flyout_bq = FloatField()
+# flyout_lf_b = FloatField()
+# flyout_rf_b = FloatField()
+# groundout_a = FloatField()
+# groundout_b = FloatField()
+# groundout_c = FloatField()
+# avg = FloatField(null=True)
+# obp = FloatField(null=True)
+# slg = FloatField(null=True)
+#
+#
+# class PitcherRatings(BaseModelScout):
+# id = CharField(unique=True, primary_key=True)
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# vs_hand = CharField()
+# is_prep = BooleanField()
+# homerun = FloatField()
+# bp_homerun = FloatField()
+# triple = FloatField()
+# double_three = FloatField()
+# double_two = FloatField()
+# double_cf = FloatField()
+# single_two = FloatField()
+# single_one = FloatField()
+# single_center = FloatField()
+# bp_single = FloatField()
+# hbp = FloatField()
+# walk = FloatField()
+# strikeout = FloatField()
+# fo_slap = FloatField()
+# fo_center = FloatField()
+# groundout_a = FloatField()
+# groundout_b = FloatField()
+# xcheck_p = FloatField()
+# xcheck_c = FloatField()
+# xcheck_1b = FloatField()
+# xcheck_2b = FloatField()
+# xcheck_3b = FloatField()
+# xcheck_ss = FloatField()
+# xcheck_lf = FloatField()
+# xcheck_cf = FloatField()
+# xcheck_rf = FloatField()
+# avg = FloatField(null=True)
+# obp = FloatField(null=True)
+# slg = FloatField(null=True)
+#
+#
+# # scout_db.create_tables([BatterRatings, PitcherRatings])
+#
+#
+# class CardColumns(BaseModelScout):
+# id = CharField(unique=True, primary_key=True)
+# player = ForeignKeyField(ScoutPlayer)
+# hand = CharField()
+# b_ratings = ForeignKeyField(BatterRatings, null=True)
+# p_ratings = ForeignKeyField(PitcherRatings, null=True)
+# one_dice = CharField()
+# one_results = CharField()
+# one_splits = CharField()
+# two_dice = CharField()
+# two_results = CharField()
+# two_splits = CharField()
+# three_dice = CharField()
+# three_results = CharField()
+# three_splits = CharField()
+#
+#
+# class Position(BaseModelScout):
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# position = CharField()
+# innings = IntegerField()
+# range = IntegerField()
+# error = IntegerField()
+# arm = CharField(null=True)
+# pb = IntegerField(null=True)
+# overthrow = IntegerField(null=True)
+#
+#
+# class BatterData(BaseModelScout):
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# stealing = CharField()
+# st_low = IntegerField()
+# st_high = IntegerField()
+# st_auto = BooleanField()
+# st_jump = FloatField()
+# bunting = CharField(null=True)
+# hit_and_run = CharField(null=True)
+# running = CharField()
+#
+#
+# class PitcherData(BaseModelScout):
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# balk = IntegerField(null=True)
+# wild_pitch = IntegerField(null=True)
+# hold = CharField()
+# starter_rating = IntegerField()
+# relief_rating = IntegerField()
+# closer_rating = IntegerField(null=True)
+# batting = CharField(null=True)
+#
+#
+# scout_db.create_tables([CardColumns, Position, BatterData, PitcherData])
+#
+#
+# class CardOutput(BaseModelScout):
+# name = CharField()
+# hand = CharField()
+# positions = CharField()
+# stealing = CharField()
+# bunting = CharField()
+# hitandrun = CharField()
+# running = CharField()
+#
+#
+# scout_db.close()
+
diff --git a/app/dependencies.py b/app/dependencies.py
new file mode 100644
index 0000000..df7cc1f
--- /dev/null
+++ b/app/dependencies.py
@@ -0,0 +1,195 @@
+import datetime
+import logging
+import os
+
+import requests
+from fastapi.security import OAuth2PasswordBearer
+
+date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}'
+LOG_DATA = {
+ 'filename': f'logs/database/{date}.log',
+ 'format': '%(asctime)s - database - %(levelname)s - %(message)s',
+ 'log_level': logging.INFO if os.environ.get('LOG_LEVEL') == 'INFO' else 'WARN'
+}
+
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
+master_debug = False
+DB_URL = 'https://pd.manticorum.com/api/'
+AUTH_TOKEN = f'{os.environ.get("API_TOKEN")}'
+AUTH_HEADER = {'Authorization': f'Bearer {AUTH_TOKEN}'}
+
+
+if os.environ.get('TESTING') == 'False':
+ DB_URL = 'https://pddev.manticorum.com/api/'
+
+
+def valid_token(token):
+ return token == AUTH_TOKEN
+
+
+def int_timestamp(datetime_obj: datetime) -> int:
+ return int(datetime.datetime.timestamp(datetime_obj) * 1000)
+
+
+def mround(x, prec=2, base=.05):
+ return round(base * round(float(x) / base), prec)
+
+
+def param_char(other_params):
+ if other_params:
+ return '&'
+ else:
+ return '?'
+
+
+def get_req_url(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None):
+ req_url = f'{DB_URL}/v{api_ver}/{endpoint}{"/" if object_id is not None else ""}{object_id if object_id is not None else ""}'
+
+ if params:
+ other_params = False
+ for x in params:
+ req_url += f'{param_char(other_params)}{x[0]}={x[1]}'
+ other_params = True
+
+ return req_url
+
+
+async def db_get(endpoint: str, api_ver: int = 2, object_id: int = None, params: list = None, none_okay: bool = True,
+ timeout: int = 3):
+ req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params)
+ log_string = f'get:\n{endpoint} id: {object_id} params: {params}'
+ logging.info(log_string) if master_debug else logging.debug(log_string)
+
+ retries = 0
+ while True:
+ try:
+ resp = requests.get(req_url, timeout=timeout)
+ break
+ except requests.ReadTimeout as e:
+ logging.error(f'Get Timeout: {req_url} / retries: {retries} / timeout: {timeout}')
+ if retries > 1:
+ raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please '
+ f'hang on a few extra seconds and try again.')
+ timeout += [2, 5][retries]
+ retries += 1
+
+ if resp.status_code == 200:
+ data = resp.json()
+ log_string = f'{data}'
+ if master_debug:
+ logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ else:
+ logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ return data
+ elif none_okay:
+ data = resp.json()
+ log_string = f'{data}'
+ if master_debug:
+ logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ else:
+ logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ return None
+ else:
+ logging.warning(resp.text)
+ raise ValueError(f'DB: {resp.text}')
+
+
+async def db_patch(endpoint: str, object_id: int, params: list, api_ver: int = 2, timeout: int = 3):
+ req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id, params=params)
+ log_string = f'patch:\n{endpoint} {params}'
+ logging.info(log_string) if master_debug else logging.debug(log_string)
+
+ retries = 0
+ while True:
+ try:
+ resp = requests.patch(req_url, headers=AUTH_HEADER, timeout=timeout)
+ break
+ except requests.Timeout as e:
+ logging.error(f'Patch Timeout: {req_url} / retries: {retries} / timeout: {timeout}')
+ if retries > 1:
+ raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please '
+ f'hang on a few extra seconds and try again.')
+ timeout += [min(3, timeout), min(5, timeout)][retries]
+ retries += 1
+
+ if resp.status_code == 200:
+ data = resp.json()
+ log_string = f'{data}'
+ if master_debug:
+ logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ else:
+ logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ return data
+ else:
+ logging.warning(resp.text)
+ raise ValueError(f'DB: {resp.text}')
+
+
+async def db_post(endpoint: str, api_ver: int = 2, payload: dict = None, timeout: int = 3):
+ req_url = get_req_url(endpoint, api_ver=api_ver)
+ log_string = f'post:\n{endpoint} payload: {payload}\ntype: {type(payload)}'
+ logging.info(log_string) if master_debug else logging.debug(log_string)
+
+ retries = 0
+ while True:
+ try:
+ resp = requests.post(req_url, json=payload, headers=AUTH_HEADER, timeout=timeout)
+ break
+ except requests.Timeout as e:
+ logging.error(f'Post Timeout: {req_url} / retries: {retries} / timeout: {timeout}')
+ if retries > 1:
+ raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please '
+ f'hang on a few extra seconds and try again.')
+ timeout += [min(3, timeout), min(5, timeout)][retries]
+ retries += 1
+
+ if resp.status_code == 200:
+ data = resp.json()
+ log_string = f'{data}'
+ if master_debug:
+ logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ else:
+ logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ return data
+ else:
+ logging.warning(resp.text)
+ raise ValueError(f'DB: {resp.text}')
+
+
+async def db_delete(endpoint: str, object_id: int, api_ver: int = 2, timeout=3):
+ req_url = get_req_url(endpoint, api_ver=api_ver, object_id=object_id)
+ log_string = f'delete:\n{endpoint} {object_id}'
+ logging.info(log_string) if master_debug else logging.debug(log_string)
+
+ retries = 0
+ while True:
+ try:
+ resp = requests.delete(req_url, headers=AUTH_HEADER, timeout=timeout)
+ break
+ except requests.ReadTimeout as e:
+ logging.error(f'Delete Timeout: {req_url} / retries: {retries} / timeout: {timeout}')
+ if retries > 1:
+ raise ConnectionError(f'DB: The internet was a bit too slow for me to grab the data I needed. Please '
+ f'hang on a few extra seconds and try again.')
+ timeout += [min(3, timeout), min(5, timeout)][retries]
+ retries += 1
+
+ if resp.status_code == 200:
+ data = resp.json()
+ log_string = f'{data}'
+ if master_debug:
+ logging.info(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ else:
+ logging.debug(f'return: {log_string[:1200]}{" [ S N I P P E D ]" if len(log_string) > 1200 else ""}')
+ return True
+ else:
+ logging.warning(resp.text)
+ raise ValueError(f'DB: {resp.text}')
diff --git a/app/main.py b/app/main.py
new file mode 100644
index 0000000..05dd9b9
--- /dev/null
+++ b/app/main.py
@@ -0,0 +1,46 @@
+import os
+
+from fastapi import FastAPI
+# from fastapi.staticfiles import StaticFiles
+# from fastapi.templating import Jinja2Templates
+
+from .routers_v2 import (
+ current, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, decisions,
+ batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards,
+ battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame, stratplays)
+
+app = FastAPI(
+ responses={404: {'description': 'Not found'}}
+)
+
+# app.mount("/static", StaticFiles(directory="storage/static"), name="static")
+# templates = Jinja2Templates(directory=os.path.dirname(os.path.abspath(__file__)))
+
+app.include_router(current.router)
+app.include_router(teams.router)
+app.include_router(rarity.router)
+app.include_router(cardsets.router)
+app.include_router(players.router)
+app.include_router(packtypes.router)
+app.include_router(packs.router)
+app.include_router(cards.router)
+app.include_router(events.router)
+app.include_router(results.router)
+app.include_router(rewards.router)
+app.include_router(batstats.router)
+app.include_router(pitstats.router)
+app.include_router(notifications.router)
+app.include_router(paperdex.router)
+app.include_router(gamerewards.router)
+app.include_router(gauntletrewards.router)
+app.include_router(gauntletruns.router)
+app.include_router(battingcards.router)
+app.include_router(battingcardratings.router)
+app.include_router(pitchingcards.router)
+app.include_router(pitchingcardratings.router)
+app.include_router(cardpositions.router)
+app.include_router(scouting.router)
+app.include_router(mlbplayers.router)
+app.include_router(stratgame.router)
+app.include_router(stratplays.router)
+app.include_router(decisions.router)
diff --git a/app/player_scouting.py b/app/player_scouting.py
new file mode 100644
index 0000000..c6fcb40
--- /dev/null
+++ b/app/player_scouting.py
@@ -0,0 +1,27 @@
+from typing import Literal, Optional
+from pybaseball import playerid_reverse_lookup
+
+import pydantic
+
+
+class PlayerIds(pydantic.BaseModel):
+ bbref: str = None
+ fangraphs: int = None
+ retro: str = None
+ mlbam: int = None
+
+
+def get_player_ids(player_id: str, id_type: Literal['bbref', 'fangraphs']) -> PlayerIds | None:
+ q = playerid_reverse_lookup([player_id], key_type=id_type)
+ if len(q.values) == 0:
+ return None
+ else:
+ return PlayerIds(
+ bbref=q.loc[0].key_bbref,
+ fangraphs=q.loc[0].key_fangraphs,
+ retro=q.loc[0].key_retro,
+ mlbam=q.loc[0].key_mlbam
+ )
+
+
+
diff --git a/app/routers_v2/__init__.py b/app/routers_v2/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/routers_v2/admin.py b/app/routers_v2/admin.py
new file mode 100644
index 0000000..b3b84be
--- /dev/null
+++ b/app/routers_v2/admin.py
@@ -0,0 +1,35 @@
+from fastapi import APIRouter, Depends, HTTPException
+import logging
+
+from ..db_engine import db, Player
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/admin',
+ tags=['admin']
+)
+
+
+@router.post('/stl-fix')
+async def stl_cardinals_fix(token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post. This event has been logged.'
+ )
+
+ p_query = Player.update(mlbclub='St Louis Cardinals', franchise='St Louis Cardinals').where(
+ Player.mlbclub == 'St. Louis Cardinals'
+ ).execute()
+ db.close()
+
+ return {'detail': f'Removed the period from St Louis'}
+
diff --git a/app/routers_v2/awards.py b/app/routers_v2/awards.py
new file mode 100644
index 0000000..8d5b85a
--- /dev/null
+++ b/app/routers_v2/awards.py
@@ -0,0 +1,155 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Award, model_to_dict
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/awards',
+ tags=['awards']
+)
+
+
+class AwardModel(pydantic.BaseModel):
+ name: str
+ season: int
+ timing: str = 'In-Season'
+ card_id: Optional[int] = None
+ team_id: Optional[int] = None
+ image: Optional[str] = None
+
+
+@app.get('/api/v1/awards')
+async def get_awards(
+ name: Optional[str] = None, season: Optional[int] = None, timing: Optional[str] = None,
+ card_id: Optional[int] = None, team_id: Optional[int] = None, image: Optional[str] = None,
+ csv: Optional[bool] = None):
+ all_awards = Award.select()
+
+ if all_awards.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no awards to filter')
+
+ if name is not None:
+ all_awards = all_awards.where(Award.name == name)
+ if season is not None:
+ all_awards = all_awards.where(Award.season == season)
+ if timing is not None:
+ all_awards = all_awards.where(Award.timing == timing)
+ if card_id is not None:
+ all_awards = all_awards.where(Award.card_id == card_id)
+ if team_id is not None:
+ all_awards = all_awards.where(Award.team_id == team_id)
+ if image is not None:
+ all_awards = all_awards.where(Award.image == image)
+
+ if csv:
+ data_list = [['id', 'name', 'season', 'timing', 'card', 'team', 'image']]
+ for line in all_awards:
+ data_list.append([
+ line.id, line.name, line.season, line.timing, line.card, line.team, line.image
+ ])
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_awards.count(), 'awards': []}
+ for x in all_awards:
+ return_val['awards'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@app.get('/api/v1/awards/{award_id}')
+async def get_one_award(award_id, csv: Optional[bool] = None):
+ try:
+ this_award = Award.get_by_id(award_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No award found with id {award_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'name', 'season', 'timing', 'card', 'team', 'image'],
+ [this_award.id, this_award.name, this_award.season, this_award.timing, this_award.card,
+ this_award.team, this_award.image]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_award)
+ db.close()
+ return return_val
+
+
+@app.post('/api/v1/awards')
+async def post_awards(award: AwardModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post awards. This event has been logged.'
+ )
+
+ this_award = Award(
+ name=award.name,
+ season=award.season,
+ timing=award.season,
+ card_id=award.card_id,
+ team_id=award.team_id,
+ image=award.image
+ )
+
+ saved = this_award.save()
+ if saved == 1:
+ return_val = model_to_dict(this_award)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that roster'
+ )
+
+
+@app.delete('/api/v1/awards/{award_id}')
+async def delete_award(award_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete awards. This event has been logged.'
+ )
+ try:
+ this_award = Award.get_by_id(award_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No award found with id {award_id}')
+
+ count = this_award.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Award {award_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Award {award_id} was not deleted')
+
+
diff --git a/app/routers_v2/batstats.py b/app/routers_v2/batstats.py
new file mode 100644
index 0000000..8618520
--- /dev/null
+++ b/app/routers_v2/batstats.py
@@ -0,0 +1,459 @@
+from datetime import datetime
+
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional, List
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, BattingStat, model_to_dict, fn, Card, Player, Current
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/batstats',
+ tags=['batstats']
+)
+
+
+class BatStat(pydantic.BaseModel):
+ card_id: int
+ team_id: int
+ roster_num: int
+ vs_team_id: int
+ pos: str
+ pa: Optional[int] = 0
+ ab: Optional[int] = 0
+ run: Optional[int] = 0
+ hit: Optional[int] = 0
+ rbi: Optional[int] = 0
+ double: Optional[int] = 0
+ triple: Optional[int] = 0
+ hr: Optional[int] = 0
+ bb: Optional[int] = 0
+ so: Optional[int] = 0
+ hbp: Optional[int] = 0
+ sac: Optional[int] = 0
+ ibb: Optional[int] = 0
+ gidp: Optional[int] = 0
+ sb: Optional[int] = 0
+ cs: Optional[int] = 0
+ bphr: Optional[int] = 0
+ bpfo: Optional[int] = 0
+ bp1b: Optional[int] = 0
+ bplo: Optional[int] = 0
+ xch: Optional[int] = 0
+ xhit: Optional[int] = 0
+ error: Optional[int] = 0
+ pb: Optional[int] = 0
+ sbc: Optional[int] = 0
+ csc: Optional[int] = 0
+ week: int
+ season: int
+ created: Optional[int] = int(datetime.timestamp(datetime.now())*100000)
+ game_id: int
+
+
+class BattingStatModel(pydantic.BaseModel):
+ stats: List[BatStat]
+
+
+@router.get('')
+async def get_batstats(
+ card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None,
+ season: int = None, week_start: int = None, week_end: int = None, created: int = None, csv: bool = None):
+ all_stats = BattingStat.select().join(Card).join(Player)
+
+ if season is not None:
+ all_stats = all_stats.where(BattingStat.season == season)
+ else:
+ curr = Current.latest()
+ all_stats = all_stats.where(BattingStat.season == curr.season)
+
+ if card_id is not None:
+ all_stats = all_stats.where(BattingStat.card_id == card_id)
+ if player_id is not None:
+ all_stats = all_stats.where(BattingStat.card.player.player_id == player_id)
+ if team_id is not None:
+ all_stats = all_stats.where(BattingStat.team_id == team_id)
+ if vs_team_id is not None:
+ all_stats = all_stats.where(BattingStat.vs_team_id == vs_team_id)
+ if week is not None:
+ all_stats = all_stats.where(BattingStat.week == week)
+ if week_start is not None:
+ all_stats = all_stats.where(BattingStat.week >= week_start)
+ if week_end is not None:
+ all_stats = all_stats.where(BattingStat.week <= week_end)
+ if created is not None:
+ all_stats = all_stats.where(BattingStat.created == created)
+
+ # if all_stats.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No batting stats found')
+
+ if csv:
+ data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'pos', 'pa', 'ab', 'run', 'hit', 'rbi', 'double',
+ 'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp', 'sb', 'cs', 'bphr', 'bpfo', 'bp1b',
+ 'bplo', 'xch', 'xhit', 'error', 'pb', 'sbc', 'csc', 'week', 'season', 'created', 'game_id', 'roster_num']]
+ for line in all_stats:
+ data_list.append(
+ [
+ line.id, line.card.id, line.card.player.player_id, line.card.player.cardset.name, line.team.abbrev, line.vs_team.abbrev,
+ line.pos, line.pa, line.ab, line.run, line.hit, line.rbi, line.double, line.triple, line.hr,
+ line.bb, line.so, line.hbp, line.sac, line.ibb, line.gidp, line.sb, line.cs, line.bphr, line.bpfo,
+ line.bp1b, line.bplo, line.xch, line.xhit, line.error, line.pb, line.sbc, line.csc, line.week,
+ line.season, line.created, line.game_id, line.roster_num
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_stats.count(), 'stats': []}
+ for x in all_stats:
+ return_val['stats'].append(model_to_dict(x, recurse=False))
+
+ db.close()
+ return return_val
+
+
+@router.get('/player/{player_id}')
+async def get_player_stats(
+ player_id: int, team_id: int = None, vs_team_id: int = None, week_start: int = None, week_end: int = None,
+ csv: bool = None):
+ all_stats = (BattingStat
+ .select(fn.COUNT(BattingStat.created).alias('game_count'))
+ .join(Card)
+ .group_by(BattingStat.card)
+ .where(BattingStat.card.player == player_id)).scalar()
+
+ if team_id is not None:
+ all_stats = all_stats.where(BattingStat.team_id == team_id)
+ if vs_team_id is not None:
+ all_stats = all_stats.where(BattingStat.vs_team_id == vs_team_id)
+ if week_start is not None:
+ all_stats = all_stats.where(BattingStat.week >= week_start)
+ if week_end is not None:
+ all_stats = all_stats.where(BattingStat.week <= week_end)
+
+ if csv:
+ data_list = [
+ [
+ 'pa', 'ab', 'run', 'hit', 'rbi', 'double', 'triple', 'hr', 'bb', 'so', 'hbp', 'sac', 'ibb', 'gidp',
+ 'sb', 'cs', 'bphr', 'bpfo', 'bp1b', 'bplo', 'xch', 'xhit', 'error', 'pb', 'sbc', 'csc',
+ ],[
+ all_stats.pa_sum, all_stats.ab_sum, all_stats.run, all_stats.hit_sum, all_stats.rbi_sum,
+ all_stats.double_sum, all_stats.triple_sum, all_stats.hr_sum, all_stats.bb_sum, all_stats.so_sum,
+ all_stats.hbp_sum, all_stats.sac, all_stats.ibb_sum, all_stats.gidp_sum, all_stats.sb_sum,
+ all_stats.cs_sum, all_stats.bphr_sum, all_stats.bpfo_sum, all_stats.bp1b_sum, all_stats.bplo_sum,
+ all_stats.xch, all_stats.xhit_sum, all_stats.error_sum, all_stats.pb_sum, all_stats.sbc_sum,
+ all_stats.csc_sum
+ ]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ logging.debug(f'stat pull query: {all_stats}\n')
+ # logging.debug(f'result 0: {all_stats[0]}\n')
+ for x in all_stats:
+ logging.debug(f'this_line: {model_to_dict(x)}')
+ return_val = model_to_dict(all_stats[0])
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_batstats(stats: BattingStatModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post stats. This event has been logged.'
+ )
+
+ new_stats = []
+ for x in stats.stats:
+ this_stat = BattingStat(
+ card_id=x.card_id,
+ team_id=x.team_id,
+ roster_num=x.roster_num,
+ vs_team_id=x.vs_team_id,
+ pos=x.pos,
+ pa=x.pa,
+ ab=x.ab,
+ run=x.run,
+ hit=x.hit,
+ rbi=x.rbi,
+ double=x.double,
+ triple=x.triple,
+ hr=x.hr,
+ bb=x.bb,
+ so=x.so,
+ hbp=x.hbp,
+ sac=x.sac,
+ ibb=x.ibb,
+ gidp=x.gidp,
+ sb=x.sb,
+ cs=x.cs,
+ bphr=x.bphr,
+ bpfo=x.bpfo,
+ bp1b=x.bp1b,
+ bplo=x.bplo,
+ xch=x.xch,
+ xhit=x.xhit,
+ error=x.error,
+ pb=x.pb,
+ sbc=x.sbc,
+ csc=x.csc,
+ week=x.week,
+ season=x.season,
+ created=x.created,
+ game_id=x.game_id
+ )
+ new_stats.append(this_stat)
+
+ with db.atomic():
+ BattingStat.bulk_create(new_stats, batch_size=15)
+ db.close()
+
+ raise HTTPException(status_code=200, detail=f'{len(new_stats)} batting lines have been added')
+
+
+@router.delete('/{stat_id}')
+async def delete_batstat(stat_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete stats. This event has been logged.'
+ )
+ try:
+ this_stat = BattingStat.get_by_id(stat_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}')
+
+ count = this_stat.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Stat {stat_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Stat {stat_id} was not deleted')
+
+
+# @app.get('/api/v1/plays/batting')
+# async def get_batting_totals(
+# player_id: list = Query(default=None), team_id: list = Query(default=None), min_pa: Optional[int] = 1,
+# season: list = Query(default=None), position: list = Query(default=None),
+# group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league'] = 'player',
+# sort: Optional[str] = None, limit: Optional[int] = None, short_output: Optional[bool] = False):
+# all_stats = BattingStat.select(
+# BattingStat.card, BattingStat.game_id, BattingStat.team, BattingStat.vs_team, BattingStat.pos,
+# BattingStat.card.player.alias('player'),
+# fn.SUM(BattingStat.pa).alias('sum_pa'), fn.SUM(BattingStat.ab).alias('sum_ab'),
+# fn.SUM(BattingStat.run).alias('sum_run'), fn.SUM(BattingStat.so).alias('sum_so'),
+# fn.SUM(BattingStat.hit).alias('sum_hit'), fn.SUM(BattingStat.rbi).alias('sum_rbi'),
+# fn.SUM(BattingStat.double).alias('sum_double'), fn.SUM(BattingStat.triple).alias('sum_triple'),
+# fn.SUM(BattingStat.hr).alias('sum_hr'), fn.SUM(BattingStat.bb).alias('sum_bb'),
+# fn.SUM(BattingStat.hbp).alias('sum_hbp'), fn.SUM(BattingStat.sac).alias('sum_sac'),
+# fn.SUM(BattingStat.ibb).alias('sum_ibb'), fn.SUM(BattingStat.gidp).alias('sum_gidp'),
+# fn.SUM(BattingStat.sb).alias('sum_sb'), fn.SUM(BattingStat.cs).alias('sum_cs'),
+# fn.SUM(BattingStat.bphr).alias('sum_bphr'), fn.SUM(BattingStat.bpfo).alias('sum_bpfo'),
+# fn.SUM(BattingStat.bp1b).alias('sum_bp1b'), fn.SUM(BattingStat.bplo).alias('sum_bplo')
+# ).having(
+# fn.SUM(BattingStat.pa) >= min_pa
+# ).join(Card)
+#
+# if player_id is not None:
+# # all_players = Player.select().where(Player.id << player_id)
+# all_cards = Card.select().where(Card.player_id << player_id)
+# all_stats = all_stats.where(BattingStat.card << all_cards)
+# if team_id is not None:
+# all_teams = Team.select().where(Team.id << team_id)
+# all_stats = all_stats.where(BattingStat.team << all_teams)
+# if season is not None:
+# all_stats = all_stats.where(BattingStat.season << season)
+# if position is not None:
+# all_stats = all_stats.where(BattingStat.pos << position)
+#
+# if group_by == 'player':
+# all_stats = all_stats.group_by(SQL('player'))
+# elif group_by == 'playerteam':
+# all_stats = all_stats.group_by(SQL('player'), BattingStat.team)
+# elif group_by == 'playergame':
+# all_stats = all_stats.group_by(SQL('player'), BattingStat.game_id)
+# elif group_by == 'team':
+# all_stats = all_stats.group_by(BattingStat.team)
+# elif group_by == 'teamgame':
+# all_stats = all_stats.group_by(BattingStat.team, BattingStat.game_id)
+# elif group_by == 'league':
+# all_stats = all_stats.group_by(BattingStat.season)
+#
+# if sort == 'pa-desc':
+# all_stats = all_stats.order_by(SQL('sum_pa').desc())
+# elif sort == 'newest':
+# all_stats = all_stats.order_by(-BattingStat.game_id)
+# elif sort == 'oldest':
+# all_stats = all_stats.order_by(BattingStat.game_id)
+#
+# if limit is not None:
+# if limit < 1:
+# limit = 1
+# all_stats = all_stats.limit(limit)
+#
+# logging.info(f'bat_plays query: {all_stats}')
+#
+# return_stats = {
+# 'count': all_stats.count(),
+# 'stats': [{
+# 'player': x.card.player_id if short_output else model_to_dict(x.card.player, recurse=False),
+# 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False),
+# 'pa': x.sum_pa,
+# 'ab': x.sum_ab,
+# 'run': x.sum_run,
+# 'hit': x.sum_hit,
+# 'rbi': x.sum_rbi,
+# 'double': x.sum_double,
+# 'triple': x.sum_triple,
+# 'hr': x.sum_hr,
+# 'bb': x.sum_bb,
+# 'so': x.sum_so,
+# 'hbp': x.sum_hbp,
+# 'sac': x.sum_sac,
+# 'ibb': x.sum_ibb,
+# 'gidp': x.sum_gidp,
+# 'sb': x.sum_sb,
+# 'cs': x.sum_cs,
+# 'bphr': x.sum_bphr,
+# 'bpfo': x.sum_bpfo,
+# 'bp1b': x.sum_bp1b,
+# 'bplo': x.sum_bplo,
+# 'avg': x.sum_hit / max(x.sum_ab, 1),
+# 'obp': (x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / max(x.sum_pa, 1),
+# 'slg': (x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 +
+# (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / max(x.sum_ab, 1),
+# 'ops': ((x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / max(x.sum_pa, 1)) +
+# ((x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 +
+# (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / max(x.sum_ab, 1)),
+# 'woba': (.69 * x.sum_bb + .72 * x.sum_hbp + .89 * (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr) +
+# 1.27 * x.sum_double + 1.62 * x.sum_triple + 2.1 * x.sum_hr) / max(x.sum_pa - x.sum_ibb, 1),
+# 'game': x.game_id
+# } for x in all_stats]
+# }
+#
+# db.close()
+# return return_stats
+#
+#
+# @app.get('/api/v1/plays/pitching')
+# async def get_pitching_totals(
+# player_id: list = Query(default=None), team_id: list = Query(default=None), season: list = Query(default=None),
+# group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league'] = 'player',
+# min_pa: Optional[int] = 1,
+# sort: Optional[str] = None, limit: Optional[int] = None, short_output: Optional[bool] = False):
+# all_stats = PitchingStat.select(
+# PitchingStat.card, PitchingStat.team, PitchingStat.game_id, PitchingStat.vs_team,
+# PitchingStat.card.player.alias('player'), fn.SUM(PitchingStat.ip).alias('sum_ip'),
+# fn.SUM(PitchingStat.hit).alias('sum_hit'), fn.SUM(PitchingStat.run).alias('sum_run'),
+# fn.SUM(PitchingStat.erun).alias('sum_erun'), fn.SUM(PitchingStat.so).alias('sum_so'),
+# fn.SUM(PitchingStat.bb).alias('sum_bb'), fn.SUM(PitchingStat.hbp).alias('sum_hbp'),
+# fn.SUM(PitchingStat.wp).alias('sum_wp'), fn.SUM(PitchingStat.balk).alias('sum_balk'),
+# fn.SUM(PitchingStat.hr).alias('sum_hr'), fn.SUM(PitchingStat.ir).alias('sum_ir'),
+# fn.SUM(PitchingStat.irs).alias('sum_irs'), fn.SUM(PitchingStat.gs).alias('sum_gs'),
+# fn.SUM(PitchingStat.win).alias('sum_win'), fn.SUM(PitchingStat.loss).alias('sum_loss'),
+# fn.SUM(PitchingStat.hold).alias('sum_hold'), fn.SUM(PitchingStat.sv).alias('sum_sv'),
+# fn.SUM(PitchingStat.bsv).alias('sum_bsv'), fn.COUNT(PitchingStat.game_id).alias('sum_games')
+# ).having(
+# fn.SUM(PitchingStat.ip) >= max(min_pa / 3, 1)
+# ).join(Card)
+#
+# if player_id is not None:
+# all_cards = Card.select().where(Card.player_id << player_id)
+# all_stats = all_stats.where(PitchingStat.card << all_cards)
+# if team_id is not None:
+# all_teams = Team.select().where(Team.id << team_id)
+# all_stats = all_stats.where(PitchingStat.team << all_teams)
+# if season is not None:
+# all_stats = all_stats.where(PitchingStat.season << season)
+#
+# if group_by == 'player':
+# all_stats = all_stats.group_by(SQL('player'))
+# elif group_by == 'playerteam':
+# all_stats = all_stats.group_by(SQL('player'), PitchingStat.team)
+# elif group_by == 'playergame':
+# all_stats = all_stats.group_by(SQL('player'), PitchingStat.game_id)
+# elif group_by == 'team':
+# all_stats = all_stats.group_by(PitchingStat.team)
+# elif group_by == 'teamgame':
+# all_stats = all_stats.group_by(PitchingStat.team, PitchingStat.game_id)
+# elif group_by == 'league':
+# all_stats = all_stats.group_by(PitchingStat.season)
+#
+# if sort == 'pa-desc':
+# all_stats = all_stats.order_by(SQL('sum_pa').desc())
+# elif sort == 'newest':
+# all_stats = all_stats.order_by(-PitchingStat.game_id)
+# elif sort == 'oldest':
+# all_stats = all_stats.order_by(PitchingStat.game_id)
+#
+# if limit is not None:
+# if limit < 1:
+# limit = 1
+# all_stats = all_stats.limit(limit)
+#
+# logging.info(f'bat_plays query: {all_stats}')
+#
+# return_stats = {
+# 'count': all_stats.count(),
+# 'stats': [{
+# 'player': x.card.player_id if short_output else model_to_dict(x.card.player, recurse=False),
+# 'team': x.team_id if short_output else model_to_dict(x.team, recurse=False),
+# 'tbf': None,
+# 'outs': round(x.sum_ip * 3),
+# 'games': x.sum_games,
+# 'gs': x.sum_gs,
+# 'win': x.sum_win,
+# 'loss': x.sum_loss,
+# 'hold': x.sum_hold,
+# 'save': x.sum_sv,
+# 'bsave': x.sum_bsv,
+# 'ir': x.sum_ir,
+# 'ir_sc': x.sum_irs,
+# 'runs': x.sum_run,
+# 'e_runs': x.sum_erun,
+# 'hits': x.sum_hit,
+# 'hr': x.sum_hr,
+# 'bb': x.sum_bb,
+# 'so': x.sum_so,
+# 'hbp': x.sum_hbp,
+# 'wp': x.sum_wp,
+# 'balk': x.sum_balk,
+# 'era': (x.sum_erun * 27) / round(x.sum_ip * 3),
+# 'whip': (x.sum_bb + x.sum_hit) / x.sum_ip,
+# 'avg': None,
+# 'obp': None,
+# 'woba': None,
+# 'k/9': x.sum_so * 9 / x.sum_ip,
+# 'bb/9': x.sum_bb * 9 / x.sum_ip,
+# 'k/bb': x.sum_so / max(x.sum_bb, .1),
+# 'game': None,
+# 'lob_2outs': None,
+# 'rbi%': None
+# } for x in all_stats]
+# }
+# db.close()
+# return return_stats
+
diff --git a/app/routers_v2/battingcardratings.py b/app/routers_v2/battingcardratings.py
new file mode 100644
index 0000000..3f16fc1
--- /dev/null
+++ b/app/routers_v2/battingcardratings.py
@@ -0,0 +1,289 @@
+from fastapi import APIRouter, Depends, HTTPException, Query, Response
+from typing import Literal, Optional, List
+import logging
+import pandas as pd
+import pydantic
+from pydantic import validator, root_validator
+
+from ..db_engine import db, BattingCardRatings, model_to_dict, chunked, BattingCard, Player, query_to_csv, Team
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/battingcardratings',
+ tags=['battingcardratings']
+)
+
+
+class BattingCardRatingsModel(pydantic.BaseModel):
+ battingcard_id: int
+ vs_hand: Literal['R', 'L', 'vR', 'vL']
+ homerun: float = 0.0
+ bp_homerun: float = 0.0
+ triple: float = 0.0
+ double_three: float = 0.0
+ double_two: float = 0.0
+ double_pull: float = 0.0
+ single_two: float = 0.0
+ single_one: float = 0.0
+ single_center: float = 0.0
+ bp_single: float = 0.0
+ hbp: float = 0.0
+ walk: float = 0.0
+ strikeout: float = 0.0
+ lineout: float = 0.0
+ popout: float = 0.0
+ flyout_a: float = 0.0
+ flyout_bq: float = 0.0
+ flyout_lf_b: float = 0.0
+ flyout_rf_b: float = 0.0
+ groundout_a: float = 0.0
+ groundout_b: float = 0.0
+ groundout_c: float = 0.0
+ avg: float = 0.0
+ obp: float = 0.0
+ slg: float = 0.0
+ pull_rate: float = 0.0
+ center_rate: float = 0.0
+ slap_rate: float = 0.0
+
+ @validator("avg", always=True)
+ def avg_validator(cls, v, values, **kwargs):
+ return (values['homerun'] + values['bp_homerun'] / 2 + values['triple'] + values['double_three'] +
+ values['double_two'] + values['double_pull'] + values['single_two'] + values['single_one'] +
+ values['single_center'] + values['bp_single'] / 2) / 108
+
+ @validator("obp", always=True)
+ def obp_validator(cls, v, values, **kwargs):
+ return ((values['hbp'] + values['walk']) / 108) + values['avg']
+
+ @validator("slg", always=True)
+ def slg_validator(cls, v, values, **kwargs):
+ return (values['homerun'] * 4 + values['bp_homerun'] * 2 + values['triple'] * 3 + values['double_three'] * 2 +
+ values['double_two'] * 2 + values['double_pull'] * 2 + values['single_two'] + values['single_one'] +
+ values['single_center'] + values['bp_single'] / 2) / 108
+
+ @root_validator
+ def validate_chance_total(cls, values):
+ total_chances = (
+ values['homerun'] + values['bp_homerun'] + values['triple'] + values['double_three'] +
+ values['double_two'] + values['double_pull'] + values['single_two'] + values['single_one'] +
+ values['single_center'] + values['bp_single'] + values['hbp'] + values['walk'] +
+ values['strikeout'] + values['lineout'] + values['popout'] + values['flyout_a'] +
+ values['flyout_bq'] + values['flyout_lf_b'] + values['flyout_rf_b'] + values['groundout_a'] +
+ values['groundout_b'] + values['groundout_c'])
+
+ if round(total_chances) != 108:
+ raise ValueError("Must have exactly 108 chances on the card")
+ return values
+
+
+class RatingsList(pydantic.BaseModel):
+ ratings: List[BattingCardRatingsModel]
+
+
+@router.get('')
+async def get_card_ratings(
+ team_id: int, ts: str, battingcard_id: list = Query(default=None), cardset_id: list = Query(default=None),
+ vs_hand: Literal['R', 'L', 'vR', 'vL'] = None, short_output: bool = False, csv: bool = False):
+ this_team = Team.get_or_none(Team.id == team_id)
+ logging.debug(f'Team: {this_team} / has_guide: {this_team.has_guide}')
+ if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
+ logging.warning(f'Team_id {team_id} attempted to pull ratings')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to pull card ratings.'
+ )
+ # elif not valid_token(token):
+ # logging.warning(f'Bad Token: {token}')
+ # db.close()
+ # raise HTTPException(
+ # status_code=401,
+ # detail='You are not authorized to pull card ratings.'
+ # )
+
+ all_ratings = BattingCardRatings.select()
+
+ if battingcard_id is not None:
+ all_ratings = all_ratings.where(BattingCardRatings.battingcard_id << battingcard_id)
+ if vs_hand is not None:
+ all_ratings = all_ratings.where(BattingCardRatings.vs_hand == vs_hand[-1])
+ if cardset_id is not None:
+ set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id)
+ set_cards = BattingCard.select(BattingCard.id).where(BattingCard.player << set_players)
+ all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards)
+
+ if csv:
+ # return_val = query_to_csv(all_ratings)
+ return_vals = [model_to_dict(x) for x in all_ratings]
+ for x in return_vals:
+ x.update(x['battingcard'])
+ x['player_id'] = x['battingcard']['player']['player_id']
+ del x['battingcard'], x['player']
+
+ db.close()
+ return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv')
+
+ else:
+ return_val = {'count': all_ratings.count(), 'ratings': [
+ model_to_dict(x, recurse=not short_output) for x in all_ratings
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/scouting')
+async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(default=None)):
+ this_team = Team.get_or_none(Team.id == team_id)
+ logging.debug(f'Team: {this_team} / has_guide: {this_team.has_guide}')
+ if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
+ logging.warning(f'Team_id {team_id} attempted to pull ratings')
+ db.close()
+ return 'Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to ' \
+ 'make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): ' \
+ 'https://ko-fi.com/manticorum/shop'
+
+ all_ratings = BattingCardRatings.select()
+ if cardset_id is not None:
+ set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id)
+ set_cards = BattingCard.select(BattingCard.id).where(BattingCard.player << set_players)
+ all_ratings = all_ratings.where(BattingCardRatings.battingcard << set_cards)
+
+ vl_query = all_ratings.where(BattingCardRatings.vs_hand == 'L')
+ vr_query = all_ratings.where(BattingCardRatings.vs_hand == 'R')
+
+ vl_vals = [model_to_dict(x) for x in vl_query]
+ for x in vl_vals:
+ x.update(x['battingcard'])
+ x['player_id'] = x['battingcard']['player']['player_id']
+ x['player_name'] = x['battingcard']['player']['p_name']
+ x['rarity'] = x['battingcard']['player']['rarity']['name']
+ x['cardset_id'] = x['battingcard']['player']['cardset']['id']
+ x['cardset_name'] = x['battingcard']['player']['cardset']['name']
+ del x['battingcard']
+ del x['player']
+
+ vr_vals = [model_to_dict(x) for x in vr_query]
+ for x in vr_vals:
+ x['player_id'] = x['battingcard']['player']['player_id']
+ del x['battingcard']
+
+ vl = pd.DataFrame(vl_vals)
+ vr = pd.DataFrame(vr_vals)
+ db.close()
+
+ output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr'))
+ first = ['player_id', 'player_name', 'cardset_name', 'rarity', 'hand', 'variant']
+ exclude = first + ['id_vl', 'id_vr', 'vs_hand_vl', 'vs_hand_vr']
+ output = output[first + [col for col in output.columns if col not in exclude]].sort_values(by=['player_id'])
+ # output = output.sort_values(by=['player_id'])
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+
+@router.get('/{ratings_id}')
+async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to pull card ratings.'
+ )
+
+ this_rating = BattingCardRatings.get_or_none(BattingCardRatings.id == ratings_id)
+ if this_rating is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'BattingCardRating id {ratings_id} not found')
+
+ r_data = model_to_dict(this_rating)
+ db.close()
+ return r_data
+
+
+@router.get('/player/{player_id}')
+async def get_player_ratings(
+ player_id: int, variant: list = Query(default=None), short_output: bool = False,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to pull card ratings.'
+ )
+
+ all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant)
+ if variant is not None:
+ all_cards = all_cards.where(BattingCard.variant << variant)
+
+ all_ratings = BattingCardRatings.select().where(BattingCardRatings.battingcard << all_cards)
+
+ return_val = {'count': all_ratings.count(), 'ratings': [
+ model_to_dict(x, recurse=not short_output) for x in all_ratings
+ ]}
+ db.close()
+ return return_val
+
+
+@router.put('')
+async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post card ratings.'
+ )
+
+ new_ratings = []
+ updates = 0
+ for x in ratings.ratings:
+ try:
+ BattingCardRatings.get(
+ (BattingCardRatings.battingcard_id == x.battingcard_id) & (BattingCardRatings.vs_hand == x.vs_hand)
+ )
+ updates += BattingCardRatings.update(x.dict()).where(
+ (BattingCardRatings.battingcard_id == x.battingcard_id) & (BattingCardRatings.vs_hand == x.vs_hand)
+ ).execute()
+ except BattingCardRatings.DoesNotExist:
+ new_ratings.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_ratings, 30):
+ BattingCardRatings.insert_many(batch).on_conflict_replace().execute()
+
+ db.close()
+ return f'Updated ratings: {updates}; new ratings: {len(new_ratings)}'
+
+
+@router.delete('/{ratings_id}')
+async def delete_rating(
+ ratings_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post card ratings.'
+ )
+
+ this_rating = BattingCardRatings.get_or_none(BattingCardRatings.id == ratings_id)
+ if this_rating is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'BattingCardRating id {ratings_id} not found')
+
+ count = this_rating.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Rating {this_rating} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Rating {this_rating} could not be deleted')
+
+
diff --git a/app/routers_v2/battingcards.py b/app/routers_v2/battingcards.py
new file mode 100644
index 0000000..418b712
--- /dev/null
+++ b/app/routers_v2/battingcards.py
@@ -0,0 +1,224 @@
+import random
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import Literal, Optional, List
+import logging
+import pydantic
+
+from ..db_engine import db, BattingCard, model_to_dict, fn, chunked, Player, MlbPlayer
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/battingcards',
+ tags=['battingcards']
+)
+
+
+class BattingCardModel(pydantic.BaseModel):
+ player_id: int
+ variant: int = 0
+ steal_low: int = 3
+ steal_high: int = 20
+ steal_auto: bool = False
+ steal_jump: float = 0
+ bunting: str = 'C'
+ hit_and_run: str = 'C'
+ running: int = 10
+ offense_col: int = None
+ hand: Literal['R', 'L', 'S'] = 'R'
+
+
+class BattingCardList(pydantic.BaseModel):
+ cards: List[BattingCardModel]
+
+
+@router.get('')
+async def get_batting_cards(
+ player_id: list = Query(default=None), player_name: list = Query(default=None),
+ cardset_id: list = Query(default=None), short_output: bool = False, limit: Optional[int] = None):
+ all_cards = BattingCard.select()
+ if player_id is not None:
+ all_cards = all_cards.where(BattingCard.player_id << player_id)
+ if cardset_id is not None:
+ all_players = Player.select().where(Player.cardset_id << cardset_id)
+ all_cards = all_cards.where(BattingCard.player << all_players)
+ if player_name is not None:
+ name_list = [x.lower() for x in player_name]
+ all_players = Player.select().where(fn.lower(Player.p_name) << name_list)
+ all_cards = all_cards.where(BattingCard.player << all_players)
+
+ if limit is not None:
+ all_cards = all_cards.limit(limit)
+
+ return_val = {'count': all_cards.count(), 'cards': [
+ model_to_dict(x, recurse=not short_output) for x in all_cards
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/{card_id}')
+async def get_one_card(card_id: int):
+ this_card = BattingCard.get_or_none(BattingCard.id == card_id)
+ if this_card is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found')
+
+ r_card = model_to_dict(this_card)
+ db.close()
+ return r_card
+
+
+@router.get('/player/{player_id}')
+async def get_player_cards(player_id: int, variant: list = Query(default=None), short_output: bool = False):
+ all_cards = BattingCard.select().where(BattingCard.player_id == player_id).order_by(BattingCard.variant)
+ if variant is not None:
+ all_cards = all_cards.where(BattingCard.variant << variant)
+
+ return_val = {'count': all_cards.count(), 'cards': [
+ model_to_dict(x, recurse=not short_output) for x in all_cards
+ ]}
+ db.close()
+ return return_val
+
+
+@router.put('')
+async def put_cards(cards: BattingCardList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post batting cards. This event has been logged.'
+ )
+
+ new_cards = []
+ updates = 0
+ logging.info(f'here!')
+
+ for x in cards.cards:
+ try:
+ old = BattingCard.get(
+ (BattingCard.player_id == x.player_id) & (BattingCard.variant == x.variant)
+ )
+
+ if x.offense_col is None:
+ x.offense_col = old.offense_col
+ updates += BattingCard.update(x.dict()).where(
+ (BattingCard.player_id == x.player_id) & (BattingCard.variant == x.variant)
+ ).execute()
+ except BattingCard.DoesNotExist:
+ if x.offense_col is None:
+ this_player = Player.get_or_none(Player.player_id == x.player_id)
+ mlb_player = MlbPlayer.get_or_none(MlbPlayer.key_bbref == this_player.bbref_id)
+ if mlb_player is not None:
+ logging.info(f'setting offense_col to {mlb_player.offense_col} for {this_player.p_name}')
+ x.offense_col = mlb_player.offense_col
+ else:
+ logging.info(f'randomly setting offense_col for {this_player.p_name}')
+ x.offense_col = random.randint(1, 3)
+ logging.debug(f'x.dict(): {x.dict()}')
+ new_cards.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_cards, 30):
+ BattingCard.insert_many(batch).on_conflict_replace().execute()
+
+ db.close()
+ return f'Updated cards: {updates}; new cards: {len(new_cards)}'
+
+
+@router.patch('/{card_id}')
+async def patch_card(
+ card_id: int, steal_low: Optional[int] = None, steal_high: Optional[int] = None,
+ steal_auto: Optional[bool] = None, steal_jump: Optional[float] = None, bunting: Optional[str] = None,
+ hit_and_run: Optional[str] = None, running: Optional[int] = None, offense_col: Optional[int] = None,
+ hand: Literal['R', 'L', 'S'] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch batting cards. This event has been logged.'
+ )
+
+ this_card = BattingCard.get_or_none(BattingCard.id == card_id)
+ if this_card is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found')
+
+ if steal_low is not None:
+ this_card.steal_low = steal_low
+ if steal_high is not None:
+ this_card.steal_high = steal_high
+ if steal_auto is not None:
+ this_card.steal_auto = steal_auto
+ if steal_jump is not None:
+ this_card.steal_jump = steal_jump
+ if bunting is not None:
+ this_card.bunting = bunting
+ if hit_and_run is not None:
+ this_card.hit_and_run = hit_and_run
+ if running is not None:
+ this_card.running = running
+ if offense_col is not None:
+ this_card.offense_col = offense_col
+ if hand is not None:
+ this_card.hand = hand
+
+ if this_card.save() == 1:
+ return_val = model_to_dict(this_card)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that card'
+ )
+
+
+@router.delete('/{card_id}')
+async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete batting cards. This event has been logged.'
+ )
+
+ this_card = BattingCard.get_or_none(BattingCard.id == card_id)
+ if this_card is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'BattingCard id {card_id} not found')
+
+ count = this_card.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Card {this_card} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Card {this_card} could not be deleted')
+
+
+@router.delete('')
+async def delete_all_cards(token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete batting cards. This event has been logged.'
+ )
+
+ d_query = BattingCard.delete()
+ d_query.execute()
+
+ return f'Deleted {d_query.count()} batting cards'
diff --git a/app/routers_v2/cardpositions.py b/app/routers_v2/cardpositions.py
new file mode 100644
index 0000000..554ee30
--- /dev/null
+++ b/app/routers_v2/cardpositions.py
@@ -0,0 +1,158 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import Literal, Optional, List
+import logging
+import pydantic
+from pydantic import root_validator
+
+from ..db_engine import db, CardPosition, model_to_dict, chunked, Player, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/cardpositions',
+ tags=['cardpositions']
+)
+
+
+class CardPositionModel(pydantic.BaseModel):
+ player_id: int
+ variant: int = 0
+ position: Literal['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
+ innings: int = 1
+ range: int = 5
+ error: int = 0
+ arm: Optional[int] = None
+ pb: Optional[int] = None
+ overthrow: Optional[int] = None
+
+ @root_validator
+ def position_validator(cls, values):
+ if values['position'] in ['C', 'LF', 'CF', 'RF'] and values['arm'] is None:
+ raise ValueError(f'{values["position"]} must have an arm rating')
+ if values['position'] == 'C' and (values['pb'] is None or values['overthrow'] is None):
+ raise ValueError('Catchers must have a pb and overthrow rating')
+ return values
+
+
+class PositionList(pydantic.BaseModel):
+ positions: List[CardPositionModel]
+
+
+@router.get('')
+async def get_card_positions(
+ player_id: list = Query(default=None), position: list = Query(default=None), min_innings: Optional[int] = 1,
+ r: list = Query(default=None), e: list = Query(default=None), arm: list = Query(default=None),
+ pb: list = Query(default=None), overthrow: list = Query(default=None), cardset_id: list = Query(default=None),
+ short_output: Optional[bool] = False, sort: Optional[str] = 'innings-desc'):
+ all_pos = CardPosition.select().where(CardPosition.innings >= min_innings).order_by(
+ CardPosition.player, CardPosition.position, CardPosition.variant
+ )
+
+ if player_id is not None:
+ all_pos = all_pos.where(CardPosition.player_id << player_id)
+ if position is not None:
+ p_list = [x.lower() for x in position]
+ all_pos = all_pos.where(fn.Lower(CardPosition.position) << p_list)
+ if r is not None:
+ all_pos = all_pos.where(CardPosition.range << r)
+ if e is not None:
+ all_pos = all_pos.where(CardPosition.error << e)
+ if arm is not None:
+ all_pos = all_pos.where(CardPosition.arm << arm)
+ if pb is not None:
+ all_pos = all_pos.where(CardPosition.pb << pb)
+ if overthrow is not None:
+ all_pos = all_pos.where(CardPosition.overthrow << overthrow)
+ if cardset_id is not None:
+ all_players = Player.select().where(Player.cardset_id << cardset_id)
+ all_pos = all_pos.where(CardPosition.player << all_players)
+
+ if sort == 'innings-desc':
+ all_pos = all_pos.order_by(CardPosition.innings.desc())
+ elif sort == 'innings-asc':
+ all_pos = all_pos.order_by(CardPosition.innings)
+ elif sort == 'range-desc':
+ all_pos = all_pos.order_by(CardPosition.range.desc())
+ elif sort == 'range-asc':
+ all_pos = all_pos.order_by(CardPosition.range)
+
+ return_val = {'count': all_pos.count(), 'positions': [
+ model_to_dict(x, recurse=not short_output) for x in all_pos
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/{position_id}')
+async def get_one_position(position_id: int):
+ this_pos = CardPosition.get_or_none(CardPosition.id == position_id)
+ if this_pos is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'CardPosition id {position_id} not found')
+
+ r_data = model_to_dict(this_pos)
+ db.close()
+ return r_data
+
+
+@router.put('')
+async def put_positions(positions: PositionList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post card positions. This event has been logged.'
+ )
+
+ new_cards = []
+ updates = 0
+
+ for x in positions.positions:
+ try:
+ CardPosition.get(
+ (CardPosition.player_id == x.player_id) & (CardPosition.variant == x.variant) &
+ (CardPosition.position == x.position)
+ )
+ updates += CardPosition.update(x.dict()).where(
+ (CardPosition.player_id == x.player_id) & (CardPosition.variant == x.variant) &
+ (CardPosition.position == x.position)
+ ).execute()
+ except CardPosition.DoesNotExist:
+ new_cards.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_cards, 30):
+ CardPosition.insert_many(batch).on_conflict_replace().execute()
+
+ db.close()
+ return f'Updated cards: {updates}; new cards: {len(new_cards)}'
+
+
+@router.delete('/{position_id}')
+async def delete_position(position_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete card positions. This event has been logged.'
+ )
+
+ this_pos = CardPosition.get_or_none(CardPosition.id == position_id)
+ if this_pos is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'CardPosition id {position_id} not found')
+
+ count = this_pos.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Card Position {this_pos} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Card Position {this_pos} could not be deleted')
diff --git a/app/routers_v2/cards.py b/app/routers_v2/cards.py
new file mode 100644
index 0000000..73d937e
--- /dev/null
+++ b/app/routers_v2/cards.py
@@ -0,0 +1,356 @@
+from fastapi import APIRouter, Depends, HTTPException, Response, Query
+from typing import Optional, List
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Card, model_to_dict, Team, Player, Pack, Paperdex, CARDSETS
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/cards',
+ tags=['cards']
+)
+
+
+class CardPydantic(pydantic.BaseModel):
+ player_id: int
+ team_id: int
+ pack_id: int
+ value: Optional[int] = 0
+
+
+class CardModel(pydantic.BaseModel):
+ cards: List[CardPydantic]
+
+
+@router.get('')
+async def get_cards(
+ player_id: Optional[int] = None, team_id: Optional[int] = None, pack_id: Optional[int] = None,
+ value: Optional[int] = None, min_value: Optional[int] = None, max_value: Optional[int] = None,
+ order_by: Optional[str] = None, limit: Optional[int] = None, dupes: Optional[bool] = None,
+ csv: Optional[bool] = None):
+ all_cards = Card.select()
+
+ # if all_cards.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'There are no cards to filter')
+
+ if team_id is not None:
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+ all_cards = all_cards.where(Card.team == this_team)
+ if player_id is not None:
+ try:
+ this_player = Player.get_by_id(player_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No player found with id {player_id}')
+ all_cards = all_cards.where(Card.player == this_player)
+ if pack_id is not None:
+ try:
+ this_pack = Pack.get_by_id(pack_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
+ all_cards = all_cards.where(Card.pack == this_pack)
+ if value is not None:
+ all_cards = all_cards.where(Card.value == value)
+ if min_value is not None:
+ all_cards = all_cards.where(Card.value >= min_value)
+ if max_value is not None:
+ all_cards = all_cards.where(Card.value <= max_value)
+ if order_by is not None:
+ if order_by.lower() == 'new':
+ all_cards = all_cards.order_by(-Card.id)
+ if limit is not None:
+ all_cards = all_cards.limit(limit)
+ if dupes:
+ if team_id is None:
+ raise HTTPException(status_code=400, detail='Dupe checking must include a team_id')
+ logging.info(f'dupe check')
+ p_query = Card.select(Card.player).where(Card.team_id == team_id)
+ seen = set()
+ dupes = []
+ for x in p_query:
+ if x.player.player_id in seen:
+ dupes.append(x.player_id)
+ else:
+ seen.add(x.player_id)
+ all_cards = all_cards.where(Card.player_id << dupes)
+
+ # if all_cards.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No cards found')
+
+ if csv:
+ data_list = [['id', 'player', 'cardset', 'rarity', 'team', 'pack', 'value']]
+ for line in all_cards:
+ data_list.append(
+ [
+ line.id, line.player.p_name, line.player.cardset, line.player.rarity, line.team.abbrev, line.pack,
+ line.value
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_cards.count(), 'cards': []}
+ for x in all_cards:
+
+ this_record = model_to_dict(x)
+ logging.debug(f'this_record: {this_record}')
+
+ this_dex = Paperdex.select().where(Paperdex.player == x)
+ this_record['player']['paperdex'] = {'count': this_dex.count(), 'paperdex': []}
+ for y in this_dex:
+ this_record['player']['paperdex']['paperdex'].append(model_to_dict(y, recurse=False))
+
+ return_val['cards'].append(this_record)
+
+ # return_val['cards'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{card_id}')
+async def v1_cards_get_one(card_id, csv: Optional[bool] = False):
+ try:
+ this_card = Card.get_by_id(card_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No card found with id {card_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'player', 'team', 'pack', 'value', 'roster1', 'roster2', 'roster3'],
+ [this_card.id, this_card.player, this_card.team.abbrev, this_card.pack, this_card.value,
+ this_card.roster1.name, this_card.roster2.name, this_card.roster3.name]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_card)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def v1_cards_post(cards: CardModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post cards. This event has been logged.'
+ )
+ last_card = Card.select(Card.id).order_by(-Card.id).limit(1)
+ lc_id = last_card[0].id
+
+ new_cards = []
+ player_ids = []
+ inc_dex = True
+ this_team = Team.get_by_id(cards.cards[0].team_id)
+ if this_team.is_ai or 'Gauntlet' in this_team.abbrev:
+ inc_dex = False
+
+ # new_dex = []
+ # now = int(datetime.timestamp(datetime.now()) * 1000)
+ for x in cards.cards:
+ this_card = Card(
+ player_id=x.player_id,
+ team_id=x.team_id,
+ pack_id=x.pack_id,
+ value=x.value
+ )
+ if inc_dex:
+ Paperdex.get_or_create(team_id=x.team_id, player_id=x.player_id)
+ player_ids.append(x.player_id)
+ new_cards.append(this_card)
+
+ with db.atomic():
+ Card.bulk_create(new_cards, batch_size=15)
+ cost_query = Player.update(cost=Player.cost + 1).where(Player.player_id << player_ids)
+ cost_query.execute()
+ # sheets.post_new_cards(SHEETS_AUTH, lc_id)
+ db.close()
+
+ raise HTTPException(status_code=200, detail=f'{len(new_cards)} cards have been added')
+
+
+# @router.post('/ai-update')
+# async def v1_cards_ai_update(token: str = Depends(oauth2_scheme)):
+# if not valid_token(token):
+# logging.warning(f'Bad Token: {token}')
+# db.close()
+# raise HTTPException(
+# status_code=401,
+# detail='You are not authorized to update AI cards. This event has been logged.'
+# )
+#
+# sheets.send_ai_cards(SHEETS_AUTH)
+# raise HTTPException(status_code=200, detail=f'Just sent AI cards to sheets')
+
+
+@router.post('/legal-check/{rarity_name}')
+async def v1_cards_legal_check(
+ rarity_name: str, card_id: list = Query(default=None), token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='Unauthorized'
+ )
+ if rarity_name not in CARDSETS.keys():
+ return f'Rarity name {rarity_name} not a valid check'
+
+ bad_cards = []
+ all_cards = Card.select().where(Card.id << card_id)
+
+ for x in all_cards:
+ if x.player.cardset_id not in CARDSETS[rarity_name]:
+ if x.player.p_name in x.player.description:
+ bad_cards.append(x.player.description)
+ else:
+ bad_cards.append(f'{x.player.description} {x.player.p_name}')
+
+ return {'count': len(bad_cards), 'bad_cards': bad_cards}
+
+
+@router.post('/post-update/{starting_id}')
+async def v1_cards_post_update(starting_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to update card lists. This event has been logged.'
+ )
+
+ # sheets.post_new_cards(SHEETS_AUTH, starting_id)
+ db.close()
+ raise HTTPException(status_code=200, detail=f'Just sent cards to sheets starting at ID {starting_id}')
+
+
+@router.post('/post-delete')
+async def v1_cards_post_delete(del_ids: str, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete card lists. This event has been logged.'
+ )
+
+ logging.info(f'del_ids: {del_ids} / type: {type(del_ids)}')
+ # sheets.post_deletion(SHEETS_AUTH, del_ids.split(','))
+
+
+@router.post('/wipe-team/{team_id}')
+async def v1_cards_wipe_team(team_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to wipe teams. This event has been logged.'
+ )
+
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception as e:
+ logging.error(f'/cards/wipe-team/{team_id} - could not find team')
+ raise HTTPException(status_code=404, detail=f'Team {team_id} not found')
+
+ t_query = Card.update(team=None).where(Card.team == this_team).execute()
+ db.close()
+ return f'Wiped {t_query} cards'
+
+
+@router.patch('/{card_id}')
+async def v1_cards_patch(
+ card_id, player_id: Optional[int] = None, team_id: Optional[int] = None, pack_id: Optional[int] = None,
+ value: Optional[int] = None, roster1_id: Optional[int] = None, roster2_id: Optional[int] = None,
+ roster3_id: Optional[int] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch cards. This event has been logged.'
+ )
+ try:
+ this_card = Card.get_by_id(card_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No card found with id {card_id}')
+
+ if player_id is not None:
+ this_card.player_id = player_id
+ if team_id is not None:
+ if team_id == 0:
+ this_card.team_id = None
+ else:
+ this_card.team_id = team_id
+ if pack_id is not None:
+ this_card.pack_id = pack_id
+ if value is not None:
+ this_card.value = value
+ if roster1_id is not None:
+ this_card.roster1_id = roster1_id
+ if roster2_id is not None:
+ this_card.roster2_id = roster2_id
+ if roster3_id is not None:
+ this_card.roster3_id = roster3_id
+
+ if this_card.save() == 1:
+ return_val = model_to_dict(this_card)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{card_id}')
+async def v1_cards_delete(card_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete packs. This event has been logged.'
+ )
+ try:
+ this_card = Card.get_by_id(card_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cards found with id {card_id}')
+
+ count = this_card.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Card {card_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Card {card_id} was not deleted')
diff --git a/app/routers_v2/cardsets.py b/app/routers_v2/cardsets.py
new file mode 100644
index 0000000..b9806d4
--- /dev/null
+++ b/app/routers_v2/cardsets.py
@@ -0,0 +1,204 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Cardset, model_to_dict, fn, Event
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/cardsets',
+ tags=['cardsets']
+)
+
+
+class CardsetModel(pydantic.BaseModel):
+ name: str
+ description: str
+ event_id: Optional[int] = None
+ in_packs: Optional[bool] = True
+ total_cards: int = 0
+ for_purchase: Optional[bool] = True
+ ranked_legal: Optional[bool] = True
+
+
+@router.get('')
+async def get_cardsets(
+ name: Optional[str] = None, in_desc: Optional[str] = None, event_id: Optional[int] = None,
+ in_packs: Optional[bool] = None, ranked_legal: Optional[bool] = None, csv: Optional[bool] = None):
+ all_cardsets = Cardset.select()
+
+ if all_cardsets.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no cardsets to filter')
+
+ if name is not None:
+ all_cardsets = all_cardsets.where(fn.Lower(Cardset.name) == name.lower())
+ if in_desc is not None:
+ all_cardsets = all_cardsets.where(fn.Lower(Cardset.description).contains(in_desc.lower()))
+ if event_id is not None:
+ try:
+ this_event = Event.get_by_id(event_id)
+ all_cardsets = all_cardsets.where(Cardset.event == this_event)
+ except Exception as e:
+ logging.error(f'Failed to find event {event_id}: {e}')
+ raise HTTPException(status_code=404, detail=f'Event id {event_id} not found')
+ if in_packs is not None:
+ all_cardsets = all_cardsets.where(Cardset.in_packs == in_packs)
+ if ranked_legal is not None:
+ all_cardsets = all_cardsets.where(Cardset.ranked_legal == ranked_legal)
+
+ if all_cardsets.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cardsets found')
+
+ if csv:
+ data_list = [[
+ 'id', 'name', 'description', 'event_id', 'in_packs', 'for_purchase', 'total_cards', 'ranked_legal'
+ ]]
+ for line in all_cardsets:
+ data_list.append(
+ [
+ line.id, line.name, line.description, line.event.id if line.event else '', line.in_packs,
+ line.for_purchase, line.total_cards, line.ranked_legal
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_cardsets.count(), 'cardsets': []}
+ for x in all_cardsets:
+ return_val['cardsets'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{cardset_id}')
+async def get_one_cardset(cardset_id, csv: Optional[bool] = False):
+ try:
+ this_cardset = Cardset.get_by_id(cardset_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'name', 'description'],
+ [this_cardset.id, this_cardset.name, this_cardset.description]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+ else:
+ return_val = model_to_dict(this_cardset)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_cardsets(cardset: CardsetModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post cardsets. This event has been logged.'
+ )
+
+ dupe_set = Cardset.get_or_none(Cardset.name == cardset.name)
+ if dupe_set:
+ db.close()
+ raise HTTPException(status_code=400, detail=f'There is already a cardset using {cardset.name}')
+
+ this_cardset = Cardset(**cardset.__dict__)
+
+ saved = this_cardset.save()
+ if saved == 1:
+ return_val = model_to_dict(this_cardset)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that cardset'
+ )
+
+
+@router.patch('/{cardset_id}')
+async def patch_cardsets(
+ cardset_id, name: Optional[str] = None, description: Optional[str] = None, in_packs: Optional[bool] = None,
+ for_purchase: Optional[bool] = None, total_cards: Optional[int] = None, ranked_legal: Optional[bool] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch cardsets. This event has been logged.'
+ )
+ try:
+ this_cardset = Cardset.get_by_id(cardset_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
+
+ if name is not None:
+ this_cardset.name = name
+ if description is not None:
+ this_cardset.description = description
+ if in_packs is not None:
+ this_cardset.in_packs = in_packs
+ if for_purchase is not None:
+ this_cardset.for_purchase = for_purchase
+ if total_cards is not None:
+ this_cardset.total_cards = total_cards
+ if ranked_legal is not None:
+ this_cardset.ranked_legal = ranked_legal
+
+ if this_cardset.save() == 1:
+ return_val = model_to_dict(this_cardset)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{cardset_id}')
+async def delete_cardsets(cardset_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete cardsets. This event has been logged.'
+ )
+ try:
+ this_cardset = Cardset.get_by_id(cardset_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
+
+ count = this_cardset.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Cardset {cardset_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Cardset {cardset_id} was not deleted')
+
+
diff --git a/app/routers_v2/current.py b/app/routers_v2/current.py
new file mode 100644
index 0000000..e472712
--- /dev/null
+++ b/app/routers_v2/current.py
@@ -0,0 +1,166 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from pandas import DataFrame
+from typing import Optional
+import logging
+import pydantic
+
+from ..db_engine import db, Current, model_to_dict
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/current',
+ tags=['current']
+)
+
+
+class CurrentModel(pydantic.BaseModel):
+ season: int
+ week: int
+ gsheet_template: str
+ gsheet_version: str
+
+
+@router.get('')
+async def get_current(season: Optional[int] = None, csv: Optional[bool] = False):
+ if season:
+ current = Current.get_or_none(season=season)
+ else:
+ current = Current.latest()
+
+ if csv:
+ current_list = [
+ ['id', 'season', 'week'],
+ [current.id, current.season, current.week]
+ ]
+ return_val = DataFrame(current_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+ else:
+ return_val = model_to_dict(current)
+ db.close()
+ return return_val
+
+
+@router.get('/{current_id}')
+async def get_one_current(current_id, csv: Optional[bool] = False):
+ try:
+ current = Current.get_by_id(current_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No current found with id {current_id}')
+
+ if csv:
+ current_list = [
+ ['id', 'season', 'week'],
+ [current.id, current.season, current.week]
+ ]
+ return_val = DataFrame(current_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+ else:
+ return_val = model_to_dict(current)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_current(current: CurrentModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post current. This event has been logged.'
+ )
+
+ dupe_curr = Current.get_or_none(Current.season == current.season)
+ if dupe_curr:
+ db.close()
+ raise HTTPException(status_code=400, detail=f'There is already a current for season {current.season}')
+
+ this_curr = Current(
+ season=current.season,
+ week=current.week,
+ gsheet_template=current.gsheet_template,
+ gsheet_version=current.gsheet_version
+ )
+
+ saved = this_curr.save()
+ if saved == 1:
+ return_val = model_to_dict(this_curr)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team')
+
+
+@router.patch('/{current_id}')
+async def patch_current(
+ current_id: int, season: Optional[int] = None, week: Optional[int] = None,
+ gsheet_template: Optional[str] = None, gsheet_version: Optional[str] = None,
+ live_scoreboard: Optional[int] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch current. This event has been logged.'
+ )
+ try:
+ current = Current.get_by_id(current_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No current found with id {current_id}')
+
+ if season is not None:
+ current.season = season
+ if week is not None:
+ current.week = week
+ if gsheet_template is not None:
+ current.gsheet_template = gsheet_template
+ if gsheet_version is not None:
+ current.gsheet_version = gsheet_version
+ if live_scoreboard is not None:
+ current.live_scoreboard = live_scoreboard
+
+ if current.save() == 1:
+ return_val = model_to_dict(current)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that current'
+ )
+
+
+@router.delete('/{current_id}')
+async def delete_current(current_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete current. This event has been logged.'
+ )
+ try:
+ this_curr = Current.get_by_id(current_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No current found with id {current_id}')
+
+ count = this_curr.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Current {current_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Current {current_id} was not deleted')
diff --git a/app/routers_v2/decisions.py b/app/routers_v2/decisions.py
new file mode 100644
index 0000000..3dc2295
--- /dev/null
+++ b/app/routers_v2/decisions.py
@@ -0,0 +1,230 @@
+from fastapi import APIRouter, Depends, HTTPException, Query, Response
+from typing import List, Optional, Literal
+import copy
+import logging
+import pandas as pd
+import pydantic
+
+from ..db_engine import db, Decision, StratGame, Player, model_to_dict, chunked, fn, Team
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/decisions',
+ tags=['decisions']
+)
+
+
+class DecisionModel(pydantic.BaseModel):
+ game_id: int
+ season: int
+ week: int
+ pitcher_id: int
+ pitcher_team_id: int
+ win: int = 0
+ loss: int = 0
+ hold: int = 0
+ is_save: int = 0
+ is_start: bool = False
+ b_save: int = 0
+ irunners: int = 0
+ irunners_scored: int = 0
+ rest_ip: float = 0
+ rest_required: int = 0
+
+
+class DecisionList(pydantic.BaseModel):
+ decisions: List[DecisionModel]
+
+
+@router.get('')
+async def get_decisions(
+ season: list = Query(default=None), week: list = Query(default=None), team_id: list = Query(default=None),
+ win: Optional[int] = None, loss: Optional[int] = None, hold: Optional[int] = None, save: Optional[int] = None,
+ b_save: Optional[int] = None, irunners: list = Query(default=None), irunners_scored: list = Query(default=None),
+ game_type: list = Query(default=None),
+ game_id: list = Query(default=None), player_id: list = Query(default=None), csv: Optional[bool] = False,
+ limit: Optional[int] = 100, page_num: Optional[int] = 1, short_output: Optional[bool] = False):
+ all_dec = Decision.select().order_by(-Decision.season, -Decision.week, -Decision.id)
+
+ if season is not None:
+ all_dec = all_dec.where(Decision.season << season)
+ if week is not None:
+ all_dec = all_dec.where(Decision.week << week)
+ if game_id is not None:
+ all_dec = all_dec.where(Decision.game_id << game_id)
+ if player_id is not None:
+ all_dec = all_dec.where(Decision.pitcher_id << player_id)
+ if team_id is not None:
+ all_dec = all_dec.where(Decision.pitcher_team_id << team_id)
+ if win is not None:
+ all_dec = all_dec.where(Decision.win == win)
+ if loss is not None:
+ all_dec = all_dec.where(Decision.loss == loss)
+ if hold is not None:
+ all_dec = all_dec.where(Decision.hold == hold)
+ if save is not None:
+ all_dec = all_dec.where(Decision.save == save)
+ if b_save is not None:
+ all_dec = all_dec.where(Decision.b_save == b_save)
+ if irunners is not None:
+ all_dec = all_dec.where(Decision.irunners << irunners)
+ if irunners_scored is not None:
+ all_dec = all_dec.where(Decision.irunners_scored << irunners_scored)
+
+ if game_type is not None:
+ all_types = [x.lower() for x in game_type]
+ all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types)
+ all_dec = all_dec.where(Decision.game << all_games)
+ if limit < 1:
+ limit = 1
+ if limit > 100:
+ limit = 100
+ all_dec = all_dec.paginate(page_num, limit)
+
+ return_dec = {
+ 'count': all_dec.count(),
+ 'decisions': [model_to_dict(x, recurse=not short_output) for x in all_dec]
+ }
+ db.close()
+
+ if csv:
+ return_vals = return_dec['decisions']
+ if len(return_vals) == 0:
+ return Response(content=pd.DataFrame().to_csv(index=False), media_type='text/csv')
+
+ for x in return_vals:
+ x['game_id'] = x['game']['id']
+ x['game_type'] = x['game']['game_type']
+ x['player_id'] = x['pitcher']['player_id']
+ x['player_name'] = x['pitcher']['p_name']
+ x['player_cardset'] = x['pitcher']['cardset']['name']
+ x['team_id'] = x['pitcher_team']['id']
+ x['team_abbrev'] = x['pitcher_team']['abbrev']
+ del x['pitcher'], x['pitcher_team'], x['game']
+
+ output = pd.DataFrame(return_vals)
+ first = ['player_id', 'player_name', 'player_cardset', 'team_id', 'team_abbrev']
+ exclude = first + ['lob_all', 'lob_all_rate', 'lob_2outs', 'rbi%']
+ output = output[first + [col for col in output.columns if col not in exclude]]
+
+ db.close()
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+ return return_dec
+
+
+@router.patch('/{decision_id}')
+async def patch_decision(
+ decision_id: int, win: Optional[int] = None, loss: Optional[int] = None, hold: Optional[int] = None,
+ save: Optional[int] = None, b_save: Optional[int] = None, irunners: Optional[int] = None,
+ irunners_scored: Optional[int] = None, rest_ip: Optional[int] = None, rest_required: Optional[int] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'patch_decision - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_dec = Decision.get_or_none(Decision.id == decision_id)
+ if this_dec is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Decision ID {decision_id} not found')
+
+ if win is not None:
+ this_dec.win = win
+ if loss is not None:
+ this_dec.loss = loss
+ if hold is not None:
+ this_dec.hold = hold
+ if save is not None:
+ this_dec.is_save = save
+ if b_save is not None:
+ this_dec.b_save = b_save
+ if irunners is not None:
+ this_dec.irunners = irunners
+ if irunners_scored is not None:
+ this_dec.irunners_scored = irunners_scored
+ if rest_ip is not None:
+ this_dec.rest_ip = rest_ip
+ if rest_required is not None:
+ this_dec.rest_required = rest_required
+
+ if this_dec.save() == 1:
+ d_result = model_to_dict(this_dec)
+ db.close()
+ return d_result
+ else:
+ db.close()
+ raise HTTPException(status_code=500, detail=f'Unable to patch decision {decision_id}')
+
+
+@router.post('')
+async def post_decisions(dec_list: DecisionList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'post_decisions - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ new_dec = []
+ for x in dec_list.decisions:
+ if StratGame.get_or_none(StratGame.id == x.game_id) is None:
+ raise HTTPException(status_code=404, detail=f'Game ID {x.game_id} not found')
+ if Player.get_or_none(Player.player_id == x.pitcher_id) is None:
+ raise HTTPException(status_code=404, detail=f'Player ID {x.pitcher_id} not found')
+ if Team.get_or_none(Team.id == x.pitcher_team_id) is None:
+ raise HTTPException(status_code=404, detail=f'Team ID {x.pitcher_team_id} not found')
+
+ new_dec.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_dec, 10):
+ Decision.insert_many(batch).on_conflict_replace().execute()
+ db.close()
+
+ return f'Inserted {len(new_dec)} decisions'
+
+
+@router.delete('/{decision_id}')
+async def delete_decision(decision_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'delete_decision - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_dec = Decision.get_or_none(Decision.id == decision_id)
+ if this_dec is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Decision ID {decision_id} not found')
+
+ count = this_dec.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Decision {decision_id} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Decision {decision_id} could not be deleted')
+
+
+@router.delete('/game/{game_id}')
+async def delete_decisions_game(game_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'delete_decisions_game - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_game = StratGame.get_or_none(StratGame.id == game_id)
+ if not this_game:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Game ID {game_id} not found')
+
+ count = Decision.delete().where(Decision.game == this_game).execute()
+ db.close()
+
+ if count > 0:
+ return f'Deleted {count} decisions matching Game ID {game_id}'
+ else:
+ raise HTTPException(status_code=500, detail=f'No decisions matching Game ID {game_id} were deleted')
+
+
+
diff --git a/app/routers_v2/events.py b/app/routers_v2/events.py
new file mode 100644
index 0000000..539014d
--- /dev/null
+++ b/app/routers_v2/events.py
@@ -0,0 +1,195 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Event, model_to_dict, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/events',
+ tags=['events']
+)
+
+
+class EventModel(pydantic.BaseModel):
+ name: str
+ short_desc: str
+ long_desc: str
+ url: Optional[str] = None
+ thumbnail: Optional[str] = None
+ active: Optional[bool] = False
+
+
+@router.get('')
+async def v1_events_get(
+ name: Optional[str] = None, in_desc: Optional[str] = None, active: Optional[bool] = None,
+ csv: Optional[bool] = None):
+ all_events = Event.select()
+
+ if name is not None:
+ all_events = all_events.where(fn.Lower(Event.name) == name.lower())
+ if in_desc is not None:
+ all_events = all_events.where(
+ (fn.Lower(Event.short_desc).contains(in_desc.lower())) |
+ (fn.Lower(Event.long_desc).contains(in_desc.lower()))
+ )
+ if active is not None:
+ all_events = all_events.where(Event.active == active)
+
+ if csv:
+ data_list = [['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active']]
+ for line in all_events:
+ data_list.append(
+ [
+ line.id, line.name, line.short_desc, line.long_desc, line.url, line.thumbnail, line.active
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_events.count(), 'events': []}
+ for x in all_events:
+ return_val['events'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{event_id}')
+async def v1_events_get_one(event_id, csv: Optional[bool] = False):
+ try:
+ this_event = Event.get_by_id(event_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'name', 'short_desc', 'long_desc', 'url', 'thumbnail', 'active'],
+ [this_event.id, this_event.name, this_event.short_desc, this_event.long_desc, this_event.url,
+ this_event.thumbnail, this_event.active]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_event)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def v1_events_post(event: EventModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post events. This event has been logged.'
+ )
+
+ dupe_event = Event.get_or_none(Event.name == event.name)
+ if dupe_event:
+ db.close()
+ raise HTTPException(status_code=400, detail=f'There is already an event using {event.name}')
+
+ this_event = Event(
+ name=event.name,
+ short_desc=event.short_desc,
+ long_desc=event.long_desc,
+ url=event.url,
+ thumbnail=event.thumbnail,
+ active=event.active
+ )
+
+ saved = this_event.save()
+ if saved == 1:
+ return_val = model_to_dict(this_event)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that cardset'
+ )
+
+
+@router.patch('/{event_id}')
+async def v1_events_patch(
+ event_id, name: Optional[str] = None, short_desc: Optional[str] = None, long_desc: Optional[str] = None,
+ url: Optional[str] = None, thumbnail: Optional[str] = None, active: Optional[bool] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch events. This event has been logged.'
+ )
+ try:
+ this_event = Event.get_by_id(event_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
+
+ if name is not None:
+ this_event.name = name
+ if short_desc is not None:
+ this_event.short_desc = short_desc
+ if long_desc is not None:
+ this_event.long_desc = long_desc
+ if url is not None:
+ this_event.url = url
+ if thumbnail is not None:
+ this_event.thumbnail = thumbnail
+ if active is not None:
+ this_event.active = active
+
+ if this_event.save() == 1:
+ return_val = model_to_dict(this_event)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that event'
+ )
+
+
+@router.delete('/{event_id}')
+async def v1_events_delete(event_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete events. This event has been logged.'
+ )
+ try:
+ this_event = Event.get_by_id(event_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No event found with id {event_id}')
+
+ count = this_event.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Event {event_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Event {event_id} was not deleted')
diff --git a/app/routers_v2/gamerewards.py b/app/routers_v2/gamerewards.py
new file mode 100644
index 0000000..5fbf6bc
--- /dev/null
+++ b/app/routers_v2/gamerewards.py
@@ -0,0 +1,192 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, GameRewards, model_to_dict
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/gamerewards',
+ tags=['gamerewards']
+)
+
+
+class GameRewardModel(pydantic.BaseModel):
+ name: str
+ pack_type_id: Optional[int] = None
+ player_id: Optional[int] = None
+ money: Optional[int] = None
+
+
+@router.get('')
+async def v1_gamerewards_get(
+ name: Optional[str] = None, pack_type_id: Optional[int] = None, player_id: Optional[int] = None,
+ money: Optional[int] = None, csv: Optional[bool] = None):
+ all_rewards = GameRewards.select()
+
+ # if all_rewards.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'There are no awards to filter')
+
+ if name is not None:
+ all_rewards = all_rewards.where(GameRewards.name == name)
+ if pack_type_id is not None:
+ all_rewards = all_rewards.where(GameRewards.pack_type_id == pack_type_id)
+ if player_id is not None:
+ all_rewards = all_rewards.where(GameRewards.player_id == player_id)
+ if money is not None:
+ all_rewards = all_rewards.where(GameRewards.money == money)
+
+ if csv:
+ data_list = [['id', 'pack_type_id', 'player_id', 'money']]
+ for line in all_rewards:
+ data_list.append([
+ line.id, line.pack_type_id if line.pack_type else None, line.player_id if line.player else None,
+ line.money
+ ])
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_rewards.count(), 'gamerewards': []}
+ for x in all_rewards:
+ return_val['gamerewards'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{gameaward_id}')
+async def v1_gamerewards_get_one(gamereward_id, csv: Optional[bool] = None):
+ try:
+ this_game_reward = GameRewards.get_by_id(gamereward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No game reward found with id {gamereward_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'pack_type_id', 'player_id', 'money'],
+ [this_game_reward.id, this_game_reward.pack_type_id if this_game_reward.pack_type else None,
+ this_game_reward.player_id if this_game_reward.player else None, this_game_reward.money]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_game_reward)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def v1_gamerewards_post(game_reward: GameRewardModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post game rewards. This event has been logged.'
+ )
+
+ this_award = GameRewards(
+ name=game_reward.name,
+ pack_type_id=game_reward.pack_type_id,
+ player_id=game_reward.player_id,
+ money=game_reward.money
+ )
+
+ saved = this_award.save()
+ if saved == 1:
+ return_val = model_to_dict(this_award)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that roster'
+ )
+
+
+@router.patch('/{game_reward_id}')
+async def v1_gamerewards_patch(
+ game_reward_id: int, name: Optional[str] = None, pack_type_id: Optional[int] = None,
+ player_id: Optional[int] = None, money: Optional[int] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch gamerewards. This event has been logged.'
+ )
+ try:
+ this_game_reward = GameRewards.get_by_id(game_reward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No game reward found with id {game_reward_id}')
+
+ if name is not None:
+ this_game_reward.name = name
+ if pack_type_id is not None:
+ if not pack_type_id:
+ this_game_reward.pack_type_id = None
+ else:
+ this_game_reward.pack_type_id = pack_type_id
+ if player_id is not None:
+ if not player_id:
+ this_game_reward.player_id = None
+ else:
+ this_game_reward.player_id = player_id
+ if money is not None:
+ if not money:
+ this_game_reward.money = None
+ else:
+ this_game_reward.money = money
+
+ if this_game_reward.save() == 1:
+ return_val = model_to_dict(this_game_reward)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{gamereward_id}')
+async def v1_gamerewards_delete(gamereward_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete awards. This event has been logged.'
+ )
+ try:
+ this_award = GameRewards.get_by_id(gamereward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No award found with id {gamereward_id}')
+
+ count = this_award.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Game Reward {gamereward_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Game Reward {gamereward_id} was not deleted')
+
diff --git a/app/routers_v2/gauntletrewards.py b/app/routers_v2/gauntletrewards.py
new file mode 100644
index 0000000..dcbaa72
--- /dev/null
+++ b/app/routers_v2/gauntletrewards.py
@@ -0,0 +1,139 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import Optional, List
+import logging
+import pydantic
+
+from ..db_engine import db, GauntletReward, model_to_dict, chunked, DatabaseError
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/gauntletrewards',
+ tags=['gauntletrewards']
+)
+
+
+class GauntletRewardModel(pydantic.BaseModel):
+ name: str
+ gauntlet_id: Optional[int] = 0
+ reward_id: Optional[int] = 0
+ win_num: Optional[int] = 0
+ loss_max: Optional[int] = 1
+
+
+class GauntletRewardList(pydantic.BaseModel):
+ rewards: List[GauntletRewardModel]
+
+
+@router.get('')
+async def v1_gauntletreward_get(
+ name: Optional[str] = None, gauntlet_id: Optional[int] = None, reward_id: list = Query(default=None),
+ win_num: Optional[int] = None, loss_max: Optional[int] = None):
+ all_rewards = GauntletReward.select()
+
+ if name is not None:
+ all_rewards = all_rewards.where(GauntletReward.name == name)
+ if gauntlet_id is not None:
+ all_rewards = all_rewards.where(GauntletReward.gauntlet_id == gauntlet_id)
+ if reward_id is not None:
+ all_rewards = all_rewards.where(GauntletReward.reward_id << reward_id)
+ if win_num is not None:
+ all_rewards = all_rewards.where(GauntletReward.win_num == win_num)
+ if loss_max is not None:
+ all_rewards = all_rewards.where(GauntletReward.loss_max >= loss_max)
+
+ all_rewards = all_rewards.order_by(-GauntletReward.loss_max, GauntletReward.win_num)
+
+ return_val = {'count': all_rewards.count(), 'rewards': []}
+ for x in all_rewards:
+ return_val['rewards'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{gauntletreward_id}')
+async def v1_gauntletreward_get_one(gauntletreward_id):
+ try:
+ this_reward = GauntletReward.get_by_id(gauntletreward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No gauntlet reward found with id {gauntletreward_id}')
+
+ return_val = model_to_dict(this_reward)
+ db.close()
+ return return_val
+
+
+@router.patch('/{gauntletreward_id}')
+async def v1_gauntletreward_patch(
+ gauntletreward_id, name: Optional[str] = None, gauntlet_id: Optional[int] = None,
+ reward_id: Optional[int] = None, win_num: Optional[int] = None, loss_max: Optional[int] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch gauntlet rewards. This event has been logged.'
+ )
+
+ this_reward = GauntletReward.get_or_none(GauntletReward.id == gauntletreward_id)
+ if this_reward is None:
+ db.close()
+ raise KeyError(f'Gauntlet Reward ID {gauntletreward_id} not found')
+
+ if gauntlet_id is not None:
+ this_reward.gauntlet_id = gauntlet_id
+ if reward_id is not None:
+ this_reward.reward_id = reward_id
+ if win_num is not None:
+ this_reward.win_num = win_num
+ if loss_max is not None:
+ this_reward.loss_max = loss_max
+ if name is not None:
+ this_reward.name = name
+
+ if this_reward.save():
+ r_curr = model_to_dict(this_reward)
+ db.close()
+ return r_curr
+ else:
+ db.close()
+ raise DatabaseError(f'Unable to patch gauntlet reward {gauntletreward_id}')
+
+
+@router.post('')
+async def v1_gauntletreward_post(gauntletreward: GauntletRewardList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post gauntlets. This event has been logged.'
+ )
+
+ all_rewards = []
+ for x in gauntletreward.rewards:
+ all_rewards.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(all_rewards, 15):
+ GauntletReward.insert_many(batch).on_conflict_replace().execute()
+ db.close()
+
+ return f'Inserted {len(all_rewards)} gauntlet rewards'
+
+
+@router.delete('/{gauntletreward_id}')
+async def v1_gauntletreward_delete(gauntletreward_id):
+ if GauntletReward.delete_by_id(gauntletreward_id) == 1:
+ return f'Deleted gauntlet reward ID {gauntletreward_id}'
+
+ raise DatabaseError(f'Unable to delete gauntlet run {gauntletreward_id}')
+
diff --git a/app/routers_v2/gauntletruns.py b/app/routers_v2/gauntletruns.py
new file mode 100644
index 0000000..c8fecf9
--- /dev/null
+++ b/app/routers_v2/gauntletruns.py
@@ -0,0 +1,169 @@
+from datetime import datetime
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import Optional
+import logging
+import pydantic
+
+from ..db_engine import db, GauntletRun, model_to_dict, DatabaseError
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/gauntletruns',
+ tags=['notifs']
+)
+
+
+class GauntletRunModel(pydantic.BaseModel):
+ team_id: int
+ gauntlet_id: int
+ wins: Optional[int] = 0
+ losses: Optional[int] = 0
+ gsheet: Optional[str] = None
+ created: Optional[int] = int(datetime.timestamp(datetime.now())*1000)
+ ended: Optional[int] = 0
+
+
+@router.get('')
+async def get_gauntletruns(
+ team_id: list = Query(default=None), wins: Optional[int] = None, wins_min: Optional[int] = None,
+ wins_max: Optional[int] = None, losses: Optional[int] = None, losses_min: Optional[int] = None,
+ losses_max: Optional[int] = None, gsheet: Optional[str] = None, created_after: Optional[int] = None,
+ created_before: Optional[int] = None, ended_after: Optional[int] = None, ended_before: Optional[int] = None,
+ is_active: Optional[bool] = None, gauntlet_id: list = Query(default=None), season: list = Query(default=None)):
+ all_gauntlets = GauntletRun.select()
+
+ if team_id is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.team_id << team_id)
+ if wins is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.wins == wins)
+ if wins_min is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.wins >= wins_min)
+ if wins_max is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.wins <= wins_max)
+ if losses is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.losses == losses)
+ if losses_min is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.losses >= losses_min)
+ if losses_max is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.losses <= losses_max)
+ if gsheet is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.gsheet == gsheet)
+ if created_after is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.created >= created_after)
+ if created_before is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.created <= created_before)
+ if ended_after is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.ended >= ended_after)
+ if ended_before is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.ended <= ended_before)
+ if is_active is not None:
+ if is_active is True:
+ all_gauntlets = all_gauntlets.where(GauntletRun.ended == 0)
+ else:
+ all_gauntlets = all_gauntlets.where(GauntletRun.ended != 0)
+ if gauntlet_id is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.gauntlet_id << gauntlet_id)
+ if season is not None:
+ all_gauntlets = all_gauntlets.where(GauntletRun.team.season << season)
+
+ return_val = {'count': all_gauntlets.count(), 'runs': []}
+ for x in all_gauntlets:
+ return_val['runs'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{gauntletrun_id}')
+async def get_one_gauntletrun(gauntletrun_id):
+ try:
+ this_gauntlet = GauntletRun.get_by_id(gauntletrun_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No gauntlet found with id {gauntletrun_id}')
+
+ return_val = model_to_dict(this_gauntlet)
+ db.close()
+ return return_val
+
+
+@router.patch('/{gauntletrun_id}')
+async def patch_gauntletrun(
+ gauntletrun_id, team_id: Optional[int] = None, wins: Optional[int] = None, losses: Optional[int] = None,
+ gsheet: Optional[str] = None, created: Optional[bool] = None, ended: Optional[bool] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch gauntlet runs. This event has been logged.'
+ )
+
+ this_run = GauntletRun.get_or_none(GauntletRun.id == gauntletrun_id)
+ if this_run is None:
+ db.close()
+ raise KeyError(f'Gauntlet Run ID {gauntletrun_id} not found')
+
+ if team_id is not None:
+ this_run.team_id = team_id
+ if wins is not None:
+ this_run.wins = wins
+ if losses is not None:
+ this_run.losses = losses
+ if gsheet is not None:
+ this_run.gsheet = gsheet
+ if created is not None:
+ if created is True:
+ this_run.created = int(datetime.timestamp(datetime.now())*1000)
+ else:
+ this_run.created = None
+ if ended is not None:
+ if ended is True:
+ this_run.ended = int(datetime.timestamp(datetime.now())*1000)
+ else:
+ this_run.ended = 0
+
+ if this_run.save():
+ r_curr = model_to_dict(this_run)
+ db.close()
+ return r_curr
+ else:
+ db.close()
+ raise DatabaseError(f'Unable to patch gauntlet run {gauntletrun_id}')
+
+
+@router.post('')
+async def post_gauntletrun(gauntletrun: GauntletRunModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post gauntlets. This event has been logged.'
+ )
+
+ this_run = GauntletRun(**gauntletrun.dict())
+
+ if this_run.save():
+ r_run = model_to_dict(this_run)
+ db.close()
+ return r_run
+ else:
+ db.close()
+ raise DatabaseError(f'Unable to post gauntlet run')
+
+
+@router.delete('/{gauntletrun_id}')
+async def delete_gauntletrun(gauntletrun_id):
+ if GauntletRun.delete_by_id(gauntletrun_id) == 1:
+ return f'Deleted gauntlet run ID {gauntletrun_id}'
+
+ raise DatabaseError(f'Unable to delete gauntlet run {gauntletrun_id}')
+
diff --git a/app/routers_v2/mlbplayers.py b/app/routers_v2/mlbplayers.py
new file mode 100644
index 0000000..cf8092c
--- /dev/null
+++ b/app/routers_v2/mlbplayers.py
@@ -0,0 +1,189 @@
+from fastapi import APIRouter, Depends, HTTPException, Response, Query
+from typing import Optional, List
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, MlbPlayer, model_to_dict, fn, chunked, query_to_csv
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/mlbplayers',
+ tags=['mlbplayers']
+)
+
+
+class PlayerModel(pydantic.BaseModel):
+ first_name: str
+ last_name: str
+ key_fangraphs: int = None
+ key_bbref: str = None
+ key_retro: str = None
+ key_mlbam: int = None
+ offense_col: int = None
+
+
+class PlayerList(pydantic.BaseModel):
+ players: List[PlayerModel]
+
+
+@router.get('')
+async def get_players(
+ full_name: list = Query(default=None), first_name: list = Query(default=None),
+ last_name: list = Query(default=None), key_fangraphs: list = Query(default=None),
+ key_bbref: list = Query(default=None), key_retro: list = Query(default=None),
+ key_mlbam: list = Query(default=None), offense_col: list = Query(default=None), csv: Optional[bool] = False):
+ all_players = MlbPlayer.select()
+
+ if full_name is not None:
+ name_list = [x.lower() for x in full_name]
+ all_players = all_players.where(
+ fn.lower(MlbPlayer.first_name) + ' ' + fn.lower(MlbPlayer.last_name) << name_list
+ )
+ if first_name is not None:
+ all_players = all_players.where(MlbPlayer.first_name << first_name)
+ if first_name is not None:
+ all_players = all_players.where(MlbPlayer.first_name << first_name)
+ if last_name is not None:
+ all_players = all_players.where(MlbPlayer.last_name << last_name)
+ if key_fangraphs is not None:
+ all_players = all_players.where(MlbPlayer.key_fangraphs << key_fangraphs)
+ if key_bbref is not None:
+ all_players = all_players.where(MlbPlayer.key_bbref << key_bbref)
+ if key_retro is not None:
+ all_players = all_players.where(MlbPlayer.key_retro << key_retro)
+ if key_mlbam is not None:
+ all_players = all_players.where(MlbPlayer.key_mlbam << key_mlbam)
+ if offense_col is not None:
+ all_players = all_players.where(MlbPlayer.offense_col << offense_col)
+
+ if csv:
+ return_val = query_to_csv(all_players)
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ return_val = {'count': all_players.count(), 'players': [
+ model_to_dict(x) for x in all_players
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/{player_id}')
+async def get_one_player(player_id: int):
+ this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id)
+ if this_player is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'MlbPlayer id {player_id} not found')
+
+ r_data = model_to_dict(this_player)
+ db.close()
+ return r_data
+
+
+@router.patch('/{player_id}')
+async def patch_player(
+ player_id: int, first_name: Optional[str] = None, last_name: Optional[str] = None,
+ key_fangraphs: Optional[str] = None, key_bbref: Optional[str] = None, key_retro: Optional[str] = None,
+ key_mlbam: Optional[str] = None, offense_col: Optional[str] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch mlb players. This event has been logged.'
+ )
+
+ this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id)
+ if this_player is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'MlbPlayer id {player_id} not found')
+
+ if first_name is not None:
+ this_player.first_name = first_name
+ if last_name is not None:
+ this_player.last_name = last_name
+ if key_fangraphs is not None:
+ this_player.key_fangraphs = key_fangraphs
+ if key_bbref is not None:
+ this_player.key_bbref = key_bbref
+ if key_retro is not None:
+ this_player.key_retro = key_retro
+ if key_mlbam is not None:
+ this_player.key_mlbam = key_mlbam
+ if offense_col is not None:
+ this_player.offense_col = offense_col
+
+ if this_player.save() == 1:
+ return_val = model_to_dict(this_player)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that player'
+ )
+
+
+@router.post('')
+async def post_players(players: PlayerList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post mlb players. This event has been logged.'
+ )
+
+ new_players = []
+ for x in players.players:
+ dupes = MlbPlayer.select().where(
+ (MlbPlayer.key_fangraphs == x.key_fangraphs) | (MlbPlayer.key_mlbam == x.key_mlbam) |
+ (MlbPlayer.key_retro == x.key_retro) | (MlbPlayer.key_bbref == x.key_bbref)
+ )
+ if dupes.count() > 0:
+ db.close()
+ raise HTTPException(
+ status_code=400,
+ detail=f'{x.first_name} {x.last_name} has a key already in the database'
+ )
+
+ new_players.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_players, 15):
+ MlbPlayer.insert_many(batch).on_conflict_replace().execute()
+ db.close()
+
+ return f'Inserted {len(new_players)} new MLB players'
+
+
+@router.delete('/{player_id}')
+async def delete_player(player_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete mlb players. This event has been logged.'
+ )
+
+ this_player = MlbPlayer.get_or_none(MlbPlayer.id == player_id)
+ if this_player is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'MlbPlayer id {player_id} not found')
+
+ count = this_player.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Player {player_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Player {player_id} was not deleted')
diff --git a/app/routers_v2/notifications.py b/app/routers_v2/notifications.py
new file mode 100644
index 0000000..4fcfa3d
--- /dev/null
+++ b/app/routers_v2/notifications.py
@@ -0,0 +1,201 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Notification, model_to_dict, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/notifs',
+ tags=['notifs']
+)
+
+
+class NotifModel(pydantic.BaseModel):
+ created: int
+ title: str
+ desc: Optional[str] = None
+ field_name: str
+ message: str
+ about: Optional[str] = 'blank'
+ ack: Optional[bool] = False
+
+
+@router.get('')
+async def get_notifs(
+ created_after: Optional[int] = None, title: Optional[str] = None, desc: Optional[str] = None,
+ field_name: Optional[str] = None, in_desc: Optional[str] = None, about: Optional[str] = None,
+ ack: Optional[bool] = None, csv: Optional[bool] = None):
+ all_notif = Notification.select()
+
+ if all_notif.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no notifications to filter')
+
+ if created_after is not None:
+ all_notif = all_notif.where(Notification.created < created_after)
+ if title is not None:
+ all_notif = all_notif.where(Notification.title == title)
+ if desc is not None:
+ all_notif = all_notif.where(Notification.desc == desc)
+ if field_name is not None:
+ all_notif = all_notif.where(Notification.field_name == field_name)
+ if in_desc is not None:
+ all_notif = all_notif.where(fn.Lower(Notification.desc).contains(in_desc.lower()))
+ if about is not None:
+ all_notif = all_notif.where(Notification.about == about)
+ if ack is not None:
+ all_notif = all_notif.where(Notification.ack == ack)
+
+ if csv:
+ data_list = [['id', 'created', 'title', 'desc', 'field_name', 'message', 'about', 'ack']]
+ for line in all_notif:
+ data_list.append([
+ line.id, line.created, line.title, line.desc, line.field_name, line.message, line.about, line.ack
+ ])
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_notif.count(), 'notifs': []}
+ for x in all_notif:
+ return_val['notifs'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{notif_id}')
+async def get_one_notif(notif_id, csv: Optional[bool] = None):
+ try:
+ this_notif = Notification.get_by_id(notif_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'created', 'title', 'desc', 'field_name', 'message', 'about', 'ack'],
+ [this_notif.id, this_notif.created, this_notif.title, this_notif.desc, this_notif.field_name,
+ this_notif.message, this_notif.about, this_notif.ack]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_notif)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_notif(notif: NotifModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post notifications. This event has been logged.'
+ )
+
+ logging.info(f'new notif: {notif}')
+ this_notif = Notification(
+ created=notif.created,
+ title=notif.title,
+ desc=notif.desc,
+ field_name=notif.field_name,
+ message=notif.message,
+ about=notif.about,
+ )
+
+ saved = this_notif.save()
+ if saved == 1:
+ return_val = model_to_dict(this_notif)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that notification'
+ )
+
+
+@router.patch('/{notif_id}')
+async def patch_notif(
+ notif_id, created: Optional[int] = None, title: Optional[str] = None, desc: Optional[str] = None,
+ field_name: Optional[str] = None, message: Optional[str] = None, about: Optional[str] = None,
+ ack: Optional[bool] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch notifications. This event has been logged.'
+ )
+ try:
+ this_notif = Notification.get_by_id(notif_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}')
+
+ if title is not None:
+ this_notif.title = title
+ if desc is not None:
+ this_notif.desc = desc
+ if field_name is not None:
+ this_notif.field_name = field_name
+ if message is not None:
+ this_notif.message = message
+ if about is not None:
+ this_notif.about = about
+ if ack is not None:
+ this_notif.ack = ack
+ if created is not None:
+ this_notif.created = created
+
+ if this_notif.save() == 1:
+ return_val = model_to_dict(this_notif)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{notif_id}')
+async def delete_notif(notif_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete notifications. This event has been logged.'
+ )
+ try:
+ this_notif = Notification.get_by_id(notif_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No notification found with id {notif_id}')
+
+ count = this_notif.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Notification {notif_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Notification {notif_id} was not deleted')
diff --git a/app/routers_v2/packs.py b/app/routers_v2/packs.py
new file mode 100644
index 0000000..aec29df
--- /dev/null
+++ b/app/routers_v2/packs.py
@@ -0,0 +1,259 @@
+from datetime import datetime
+
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional, List
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Cardset, model_to_dict, Pack, Team, PackType
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/packs',
+ tags=['packs']
+)
+
+
+class PackPydantic(pydantic.BaseModel):
+ team_id: int
+ pack_type_id: int
+ pack_team_id: Optional[int] = None
+ pack_cardset_id: Optional[int] = None
+ open_time: Optional[str] = None
+
+
+class PackModel(pydantic.BaseModel):
+ packs: List[PackPydantic]
+
+
+@router.get('')
+async def get_packs(
+ team_id: Optional[int] = None, pack_type_id: Optional[int] = None, opened: Optional[bool] = None,
+ limit: Optional[int] = None, new_to_old: Optional[bool] = None, pack_team_id: Optional[int] = None,
+ pack_cardset_id: Optional[int] = None, exact_match: Optional[bool] = False, csv: Optional[bool] = None):
+ all_packs = Pack.select()
+
+ if all_packs.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no packs to filter')
+
+ if team_id is not None:
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+ all_packs = all_packs.where(Pack.team == this_team)
+ if pack_type_id is not None:
+ try:
+ this_pack_type = PackType.get_by_id(pack_type_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No pack type found with id {pack_type_id}')
+ all_packs = all_packs.where(Pack.pack_type == this_pack_type)
+
+ if pack_team_id is not None:
+ try:
+ this_pack_team = Team.get_by_id(pack_team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {pack_team_id}')
+ all_packs = all_packs.where(Pack.pack_team == this_pack_team)
+ elif exact_match:
+ all_packs = all_packs.where(Pack.pack_team == None)
+
+ if pack_cardset_id is not None:
+ try:
+ this_pack_cardset = Cardset.get_by_id(pack_cardset_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cardset found with id {pack_cardset_id}')
+ all_packs = all_packs.where(Pack.pack_cardset == this_pack_cardset)
+ elif exact_match:
+ all_packs = all_packs.where(Pack.pack_cardset == None)
+
+ if opened is not None:
+ all_packs = all_packs.where(Pack.open_time.is_null(not opened))
+ if limit is not None:
+ all_packs = all_packs.limit(limit)
+ if new_to_old is not None:
+ all_packs = all_packs.order_by(-Pack.id)
+
+ # if all_packs.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No packs found')
+
+ if csv:
+ data_list = [['id', 'team', 'pack_type', 'open_time']]
+ for line in all_packs:
+ data_list.append(
+ [
+ line.id, line.team.abbrev, line.pack_type.name,
+ datetime.fromtimestamp(line.open_time) if line.open_time else None
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_packs.count(), 'packs': []}
+ for x in all_packs:
+ return_val['packs'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{pack_id}')
+async def get_one_pack(pack_id, csv: Optional[bool] = False):
+ try:
+ this_pack = Pack.get_by_id(pack_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'team', 'pack_type', 'open_time'],
+ [this_pack.id, this_pack.team.abbrev, this_pack.pack_type.name,
+ datetime.fromtimestamp(this_pack.open_time) if this_pack.open_time else None]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_pack)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_pack(packs: PackModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post packs. This event has been logged.'
+ )
+
+ new_packs = []
+ for x in packs.packs:
+ this_player = Pack(
+ team_id=x.team_id,
+ pack_type_id=x.pack_type_id,
+ pack_team_id=x.pack_team_id,
+ pack_cardset_id=x.pack_cardset_id,
+ open_time=x.open_time if x.open_time != "" else None
+ )
+ new_packs.append(this_player)
+
+ with db.atomic():
+ Pack.bulk_create(new_packs, batch_size=15)
+ db.close()
+
+ raise HTTPException(status_code=200, detail=f'{len(new_packs)} packs have been added')
+
+
+@router.post('/one')
+async def post_one_pack(pack: PackPydantic, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post packs. This event has been logged.'
+ )
+
+ this_pack = Pack(
+ team_id=pack.team_id,
+ pack_type_id=pack.pack_type_id,
+ pack_team_id=pack.pack_team_id,
+ pack_cardset_id=pack.pack_cardset_id,
+ open_time=pack.open_time
+ )
+
+ saved = this_pack.save()
+ if saved == 1:
+ return_val = model_to_dict(this_pack)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that cardset'
+ )
+
+
+@router.patch('/{pack_id}')
+async def patch_pack(
+ pack_id, team_id: Optional[int] = None, pack_type_id: Optional[int] = None, open_time: Optional[int] = None,
+ pack_team_id: Optional[int] = None, pack_cardset_id: Optional[int] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch packs. This event has been logged.'
+ )
+ try:
+ this_pack = Pack.get_by_id(pack_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No pack found with id {pack_id}')
+
+ if team_id is not None:
+ this_pack.team_id = team_id
+ if pack_type_id is not None:
+ this_pack.pack_type_id = pack_type_id
+ if pack_team_id is not None:
+ this_pack.pack_team_id = pack_team_id
+ if pack_cardset_id is not None:
+ this_pack.pack_cardset_id = pack_cardset_id
+ if open_time is not None:
+ this_pack.open_time = open_time
+
+ if this_pack.save() == 1:
+ return_val = model_to_dict(this_pack)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{pack_id}')
+async def delete_pack(pack_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete packs. This event has been logged.'
+ )
+ try:
+ this_pack = Pack.get_by_id(pack_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No packs found with id {pack_id}')
+
+ count = this_pack.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Pack {pack_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Pack {pack_id} was not deleted')
diff --git a/app/routers_v2/packtypes.py b/app/routers_v2/packtypes.py
new file mode 100644
index 0000000..98ae1e2
--- /dev/null
+++ b/app/routers_v2/packtypes.py
@@ -0,0 +1,194 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, PackType, model_to_dict, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/packtypes',
+ tags=['packtypes']
+)
+
+
+class PacktypeModel(pydantic.BaseModel):
+ name: str
+ card_count: int
+ description: str
+ cost: int
+ available: Optional[bool] = True
+
+
+@router.get('')
+async def get_packtypes(
+ name: Optional[str] = None, card_count: Optional[int] = None, in_desc: Optional[str] = None,
+ available: Optional[bool] = None, csv: Optional[bool] = None):
+ all_packtypes = PackType.select()
+
+ if all_packtypes.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no packtypes to filter')
+
+ if name is not None:
+ all_packtypes = all_packtypes.where(fn.Lower(PackType.name) == name.lower())
+ if card_count is not None:
+ all_packtypes = all_packtypes.where(PackType.card_count == card_count)
+ if in_desc is not None:
+ all_packtypes = all_packtypes.where(fn.Lower(PackType.description).contains(in_desc.lower()))
+ if available is not None:
+ all_packtypes = all_packtypes.where(PackType.available == available)
+
+ # if all_packtypes.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No packtypes found')
+
+ if csv:
+ data_list = [['id', 'name', 'card_count', 'description']]
+ for line in all_packtypes:
+ data_list.append(
+ [
+ line.id, line.name, line.card_count, line.description
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_packtypes.count(), 'packtypes': []}
+ for x in all_packtypes:
+ return_val['packtypes'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{packtype_id}')
+async def get_one_packtype(packtype_id, csv: Optional[bool] = False):
+ try:
+ this_packtype = PackType.get_by_id(packtype_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'name', 'card_count', 'description'],
+ [this_packtype.id, this_packtype.name, this_packtype.card_count, this_packtype.description]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_packtype)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_packtypes(packtype: PacktypeModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post packtypes. This event has been logged.'
+ )
+
+ dupe_packtype = PackType.get_or_none(PackType.name == packtype.name)
+ if dupe_packtype:
+ db.close()
+ raise HTTPException(status_code=400, detail=f'There is already a packtype using {packtype.name}')
+
+ this_packtype = PackType(
+ name=packtype.name,
+ card_count=packtype.card_count,
+ description=packtype.description,
+ cost=packtype.cost,
+ available=packtype.available
+ )
+
+ saved = this_packtype.save()
+ if saved == 1:
+ return_val = model_to_dict(this_packtype)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that cardset'
+ )
+
+
+@router.patch('/{packtype_id}')
+async def patch_packtype(
+ packtype_id, name: Optional[str] = None, card_count: Optional[int] = None, description: Optional[str] = None,
+ cost: Optional[int] = None, available: Optional[bool] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch packtypes. This event has been logged.'
+ )
+ try:
+ this_packtype = PackType.get_by_id(packtype_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}')
+
+ if name is not None:
+ this_packtype.name = name
+ if card_count is not None:
+ this_packtype.card_count = card_count
+ if description is not None:
+ this_packtype.description = description
+ if cost is not None:
+ this_packtype.cost = cost
+ if available is not None:
+ this_packtype.available = available
+
+ if this_packtype.save() == 1:
+ return_val = model_to_dict(this_packtype)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{packtype_id}')
+async def delete_packtype(packtype_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete packtypes. This event has been logged.'
+ )
+ try:
+ this_packtype = PackType.get_by_id(packtype_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No packtype found with id {packtype_id}')
+
+ count = this_packtype.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Packtype {packtype_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Packtype {packtype_id} was not deleted')
diff --git a/app/routers_v2/paperdex.py b/app/routers_v2/paperdex.py
new file mode 100644
index 0000000..5ea709e
--- /dev/null
+++ b/app/routers_v2/paperdex.py
@@ -0,0 +1,207 @@
+from datetime import datetime
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Paperdex, model_to_dict, Player, Cardset, Team
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/paperdex',
+ tags=['paperdex']
+)
+
+
+class PaperdexModel(pydantic.BaseModel):
+ team_id: int
+ player_id: int
+ created: Optional[int] = int(datetime.timestamp(datetime.now())*1000)
+
+
+@router.get('')
+async def get_paperdex(
+ team_id: Optional[int] = None, player_id: Optional[int] = None, created_after: Optional[int] = None,
+ cardset_id: Optional[int] = None, created_before: Optional[int] = None, flat: Optional[bool] = False,
+ csv: Optional[bool] = None):
+ all_dex = Paperdex.select().join(Player).join(Cardset)
+
+ if all_dex.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no paperdex to filter')
+
+ if team_id is not None:
+ all_dex = all_dex.where(Paperdex.team_id == team_id)
+ if player_id is not None:
+ all_dex = all_dex.where(Paperdex.player_id == player_id)
+ if cardset_id is not None:
+ all_sets = Cardset.select().where(Cardset.id == cardset_id)
+ all_dex = all_dex.where(Paperdex.player.cardset.id == cardset_id)
+ if created_after is not None:
+ all_dex = all_dex.where(Paperdex.created >= created_after)
+ if created_before is not None:
+ all_dex = all_dex.where(Paperdex.created <= created_before)
+
+ # if all_dex.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No paperdex found')
+
+ if csv:
+ data_list = [['id', 'team_id', 'player_id', 'created']]
+ for line in all_dex:
+ data_list.append(
+ [
+ line.id, line.team.id, line.player.player_id, line.created
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_dex.count(), 'paperdex': []}
+ for x in all_dex:
+ return_val['paperdex'].append(model_to_dict(x, recurse=not flat))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{paperdex_id}')
+async def get_one_paperdex(paperdex_id, csv: Optional[bool] = False):
+ try:
+ this_dex = Paperdex.get_by_id(paperdex_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'team_id', 'player_id', 'created'],
+ [this_dex.id, this_dex.team.id, this_dex.player.id, this_dex.created]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_dex)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_paperdex(paperdex: PaperdexModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post paperdex. This event has been logged.'
+ )
+
+ dupe_dex = Paperdex.get_or_none(Paperdex.team_id == paperdex.team_id, Paperdex.player_id == paperdex.player_id)
+ if dupe_dex:
+ return_val = model_to_dict(dupe_dex)
+ db.close()
+ return return_val
+
+ this_dex = Paperdex(
+ team_id=paperdex.team_id,
+ player_id=paperdex.player_id,
+ created=paperdex.created
+ )
+
+ saved = this_dex.save()
+ if saved == 1:
+ return_val = model_to_dict(this_dex)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that dex'
+ )
+
+
+@router.patch('/{paperdex_id}')
+async def patch_paperdex(
+ paperdex_id, team_id: Optional[int] = None, player_id: Optional[int] = None, created: Optional[int] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch paperdex. This event has been logged.'
+ )
+ try:
+ this_dex = Paperdex.get_by_id(paperdex_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
+
+ if team_id is not None:
+ this_dex.team_id = team_id
+ if player_id is not None:
+ this_dex.player_id = player_id
+ if created is not None:
+ this_dex.created = created
+
+ if this_dex.save() == 1:
+ return_val = model_to_dict(this_dex)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{paperdex_id}')
+async def delete_paperdex(paperdex_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete rewards. This event has been logged.'
+ )
+ try:
+ this_dex = Paperdex.get_by_id(paperdex_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
+
+ count = this_dex.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Paperdex {this_dex} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Paperdex {this_dex} was not deleted')
+
+
+@router.post('/wipe-ai')
+async def wipe_ai_paperdex(token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='Unauthorized'
+ )
+
+ g_teams = Team.select().where(Team.abbrev.contains('Gauntlet'))
+ count = Paperdex.delete().where(Paperdex.team << g_teams).execute()
+ return f'Deleted {count} records'
diff --git a/app/routers_v2/pitchingcardratings.py b/app/routers_v2/pitchingcardratings.py
new file mode 100644
index 0000000..344cad7
--- /dev/null
+++ b/app/routers_v2/pitchingcardratings.py
@@ -0,0 +1,267 @@
+from fastapi import APIRouter, Depends, HTTPException, Query, Response
+from typing import Literal, Optional, List
+import logging
+import pandas as pd
+import pydantic
+from pydantic import validator, root_validator
+
+from ..db_engine import db, PitchingCardRatings, model_to_dict, chunked, PitchingCard, Player, query_to_csv, Team
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/pitchingcardratings',
+ tags=['pitchingcardratings']
+)
+
+
+class PitchingCardRatingsModel(pydantic.BaseModel):
+ pitchingcard_id: int
+ vs_hand: Literal['R', 'L', 'vR', 'vL']
+ homerun: float = 0.0
+ bp_homerun: float = 0.0
+ triple: float = 0.0
+ double_three: float = 0.0
+ double_two: float = 0.0
+ double_cf: float = 0.0
+ single_two: float = 0.0
+ single_one: float = 0.0
+ single_center: float = 0.0
+ bp_single: float = 0.0
+ hbp: float = 0.0
+ walk: float = 0.0
+ strikeout: float = 0.0
+ flyout_lf_b: float = 0.0
+ flyout_cf_b: float = 0.0
+ flyout_rf_b: float = 0.0
+ groundout_a: float = 0.0
+ groundout_b: float = 0.0
+ xcheck_p: float = 0.0
+ xcheck_c: float = 0.0
+ xcheck_1b: float = 0.0
+ xcheck_2b: float = 0.0
+ xcheck_3b: float = 0.0
+ xcheck_ss: float = 0.0
+ xcheck_lf: float = 0.0
+ xcheck_cf: float = 0.0
+ xcheck_rf: float = 0.0
+ avg: float = 0.0
+ obp: float = 0.0
+ slg: float = 0.0
+
+ @validator("avg", always=True)
+ def avg_validator(cls, v, values, **kwargs):
+ return (values['homerun'] + values['bp_homerun'] / 2 + values['triple'] + values['double_three'] +
+ values['double_two'] + values['double_cf'] + values['single_two'] + values['single_one'] +
+ values['single_center'] + values['bp_single'] / 2) / 108
+
+ @validator("obp", always=True)
+ def obp_validator(cls, v, values, **kwargs):
+ return ((values['hbp'] + values['walk']) / 108) + values['avg']
+
+ @validator("slg", always=True)
+ def slg_validator(cls, v, values, **kwargs):
+ return (values['homerun'] * 4 + values['bp_homerun'] * 2 + values['triple'] * 3 + values['double_three'] * 2 +
+ values['double_two'] * 2 + values['double_cf'] * 2 + values['single_two'] + values['single_one'] +
+ values['single_center'] + values['bp_single'] / 2) / 108
+
+ @root_validator
+ def validate_chance_total(cls, values):
+ total_chances = (
+ values['homerun'] + values['bp_homerun'] + values['triple'] + values['double_three'] +
+ values['double_two'] + values['double_cf'] + values['single_two'] + values['single_one'] +
+ values['single_center'] + values['bp_single'] + values['hbp'] + values['walk'] +
+ values['strikeout'] + values['flyout_lf_b'] + values['flyout_cf_b'] + values['flyout_rf_b'] +
+ values['groundout_a'] + values['groundout_b'] + values['xcheck_p'] + values['xcheck_c'] +
+ values['xcheck_1b'] + values['xcheck_2b'] + values['xcheck_3b'] + values['xcheck_ss'] +
+ values['xcheck_lf'] + values['xcheck_cf'] + values['xcheck_rf'])
+
+ if round(total_chances) != 108:
+ raise ValueError("Must have exactly 108 chances on the card")
+ return values
+
+
+class RatingsList(pydantic.BaseModel):
+ ratings: List[PitchingCardRatingsModel]
+
+
+@router.get('')
+async def get_card_ratings(
+ pitchingcard_id: list = Query(default=None), vs_hand: Literal['R', 'L', 'vR', 'vL'] = None,
+ short_output: bool = False, csv: bool = False, cardset_id: list = Query(default=None),
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to pull card ratings.'
+ )
+
+ all_ratings = PitchingCardRatings.select()
+
+ if pitchingcard_id is not None:
+ all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard_id << pitchingcard_id)
+ if vs_hand is not None:
+ all_ratings = all_ratings.where(PitchingCardRatings.vs_hand == vs_hand[-1])
+ if cardset_id is not None:
+ set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id)
+ set_cards = PitchingCard.select(PitchingCard.id).where(PitchingCard.player << set_players)
+ all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard << set_cards)
+
+ if csv:
+ return_val = query_to_csv(all_ratings)
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_ratings.count(), 'ratings': [
+ model_to_dict(x, recurse=not short_output) for x in all_ratings
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/scouting')
+async def get_card_scouting(team_id: int, ts: str, cardset_id: list = Query(default=None)):
+ this_team = Team.get_or_none(Team.id == team_id)
+ logging.debug(f'Team: {this_team} / has_guide: {this_team.has_guide}')
+ if this_team is None or ts != this_team.team_hash() or this_team.has_guide != 1:
+ logging.warning(f'Team_id {team_id} attempted to pull ratings')
+ db.close()
+ return 'Your team does not have the ratings guide enabled. If you have purchased a copy ping Cal to ' \
+ 'make sure it is enabled on your team. If you are interested you can pick it up here (thank you!): ' \
+ 'https://ko-fi.com/manticorum/shop'
+
+ all_ratings = PitchingCardRatings.select()
+ if cardset_id is not None:
+ set_players = Player.select(Player.player_id).where(Player.cardset_id << cardset_id)
+ set_cards = PitchingCard.select(PitchingCard.id).where(PitchingCard.player << set_players)
+ all_ratings = all_ratings.where(PitchingCardRatings.pitchingcard << set_cards)
+
+ vl_query = all_ratings.where(PitchingCardRatings.vs_hand == 'L')
+ vr_query = all_ratings.where(PitchingCardRatings.vs_hand == 'R')
+
+ vl_vals = [model_to_dict(x) for x in vl_query]
+ for x in vl_vals:
+ x.update(x['pitchingcard'])
+ x['player_id'] = x['pitchingcard']['player']['player_id']
+ x['player_name'] = x['pitchingcard']['player']['p_name']
+ x['rarity'] = x['pitchingcard']['player']['rarity']['name']
+ x['cardset_id'] = x['pitchingcard']['player']['cardset']['id']
+ x['cardset_name'] = x['pitchingcard']['player']['cardset']['name']
+ del x['pitchingcard']
+ del x['player']
+
+ vr_vals = [model_to_dict(x) for x in vr_query]
+ for x in vr_vals:
+ x['player_id'] = x['pitchingcard']['player']['player_id']
+ del x['pitchingcard']
+
+ vl = pd.DataFrame(vl_vals)
+ vr = pd.DataFrame(vr_vals)
+ db.close()
+
+ output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr'))
+ first = ['player_id', 'player_name', 'cardset_name', 'rarity', 'hand', 'variant']
+ exclude = first + ['id_vl', 'id_vr', 'vs_hand_vl', 'vs_hand_vr']
+ output = output[first + [col for col in output.columns if col not in exclude]].sort_values(by=['player_id'])
+ # output = output.sort_values(by=['player_id'])
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+
+@router.get('/{ratings_id}')
+async def get_one_rating(ratings_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to pull card ratings.'
+ )
+
+ this_rating = PitchingCardRatings.get_or_none(PitchingCardRatings.id == ratings_id)
+ if this_rating is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'PitchingCardRating id {ratings_id} not found')
+
+ r_data = model_to_dict(this_rating)
+ db.close()
+ return r_data
+
+
+@router.get('/player/{player_id}')
+async def get_player_ratings(player_id: int, variant: list = Query(default=None), short_output: bool = False):
+ all_cards = PitchingCard.select().where(PitchingCard.player_id == player_id).order_by(PitchingCard.variant)
+ if variant is not None:
+ all_cards = all_cards.where(PitchingCard.variant << variant)
+
+ all_ratings = PitchingCardRatings.select().where(PitchingCardRatings.pitchingcard << all_cards)
+
+ return_val = {'count': all_ratings.count(), 'ratings': [
+ model_to_dict(x, recurse=not short_output) for x in all_ratings
+ ]}
+ db.close()
+ return return_val
+
+
+@router.put('')
+async def put_ratings(ratings: RatingsList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post card ratings.'
+ )
+
+ new_ratings = []
+ updates = 0
+ for x in ratings.ratings:
+ try:
+ PitchingCardRatings.get(
+ (PitchingCardRatings.pitchingcard_id == x.pitchingcard_id) & (PitchingCardRatings.vs_hand == x.vs_hand)
+ )
+ updates += PitchingCardRatings.update(x.dict()).where(
+ (PitchingCardRatings.pitchingcard_id == x.pitchingcard_id) & (PitchingCardRatings.vs_hand == x.vs_hand)
+ ).execute()
+ except PitchingCardRatings.DoesNotExist:
+ new_ratings.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_ratings, 30):
+ PitchingCardRatings.insert_many(batch).on_conflict_replace().execute()
+
+ db.close()
+ return f'Updated ratings: {updates}; new ratings: {len(new_ratings)}'
+
+
+@router.delete('/{ratings_id}')
+async def delete_rating(
+ ratings_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post card ratings.'
+ )
+
+ this_rating = PitchingCardRatings.get_or_none(PitchingCardRatings.id == ratings_id)
+ if this_rating is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'PitchingCardRating id {ratings_id} not found')
+
+ count = this_rating.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Rating {this_rating} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Rating {this_rating} could not be deleted')
+
diff --git a/app/routers_v2/pitchingcards.py b/app/routers_v2/pitchingcards.py
new file mode 100644
index 0000000..77afd6c
--- /dev/null
+++ b/app/routers_v2/pitchingcards.py
@@ -0,0 +1,218 @@
+import random
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from typing import Literal, Optional, List
+import logging
+import pydantic
+
+from ..db_engine import db, PitchingCard, model_to_dict, chunked, Player, fn, MlbPlayer
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/pitchingcards',
+ tags=['pitchingcards']
+)
+
+
+class PitchingCardModel(pydantic.BaseModel):
+ player_id: int
+ variant: int = 0
+ balk: int = 0
+ wild_pitch: int = 0
+ hold: int = 0
+ starter_rating: int = 1
+ relief_rating: int = 0
+ closer_rating: int = None
+ batting: str = "#1WR-C"
+ offense_col: int = None
+ hand: Literal['R', 'L', 'S'] = 'R'
+
+
+class PitchingCardList(pydantic.BaseModel):
+ cards: List[PitchingCardModel]
+
+
+@router.get('')
+async def get_pitching_cards(
+ player_id: list = Query(default=None), player_name: list = Query(default=None),
+ cardset_id: list = Query(default=None), short_output: bool = False, limit: Optional[int] = None):
+ all_cards = PitchingCard.select()
+ if player_id is not None:
+ all_cards = all_cards.where(PitchingCard.player_id << player_id)
+ if cardset_id is not None:
+ all_players = Player.select().where(Player.cardset_id << cardset_id)
+ all_cards = all_cards.where(PitchingCard.player << all_players)
+ if player_name is not None:
+ name_list = [x.lower() for x in player_name]
+ all_players = Player.select().where(fn.lower(Player.p_name) << name_list)
+ all_cards = all_cards.where(PitchingCard.player << all_players)
+
+ if limit is not None:
+ all_cards = all_cards.limit(limit)
+
+ return_val = {'count': all_cards.count(), 'cards': [
+ model_to_dict(x, recurse=not short_output) for x in all_cards
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/{card_id}')
+async def get_one_card(card_id: int):
+ this_card = PitchingCard.get_or_none(PitchingCard.id == card_id)
+ if this_card is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'PitchingCard id {card_id} not found')
+
+ r_card = model_to_dict(this_card)
+ db.close()
+ return r_card
+
+
+@router.get('/player/{player_id}')
+async def get_player_cards(player_id: int, variant: list = Query(default=None), short_output: bool = False):
+ all_cards = PitchingCard.select().where(PitchingCard.player_id == player_id).order_by(PitchingCard.variant)
+ if variant is not None:
+ all_cards = all_cards.where(PitchingCard.variant << variant)
+
+ return_val = {'count': all_cards.count(), 'cards': [
+ model_to_dict(x, recurse=not short_output) for x in all_cards
+ ]}
+ db.close()
+ return return_val
+
+
+@router.put('')
+async def put_cards(cards: PitchingCardList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post pitching cards. This event has been logged.'
+ )
+
+ new_cards = []
+ updates = 0
+
+ for x in cards.cards:
+ try:
+ old = PitchingCard.get(
+ (PitchingCard.player_id == x.player_id) & (PitchingCard.variant == x.variant)
+ )
+
+ if x.offense_col is None:
+ x.offense_col = old.offense_col
+ updates += PitchingCard.update(x.dict()).where(
+ (PitchingCard.player_id == x.player_id) & (PitchingCard.variant == x.variant)
+ ).execute()
+ except PitchingCard.DoesNotExist:
+ if x.offense_col is None:
+ this_player = Player.get_or_none(Player.player_id == x.player_id)
+ mlb_player = MlbPlayer.get_or_none(MlbPlayer.key_bbref == this_player.bbref_id)
+ if mlb_player is not None:
+ logging.info(f'setting offense_col to {mlb_player.offense_col} for {this_player.p_name}')
+ x.offense_col = mlb_player.offense_col
+ else:
+ logging.info(f'randomly setting offense_col for {this_player.p_name}')
+ x.offense_col = random.randint(1, 3)
+ logging.debug(f'x.dict(): {x.dict()}')
+ new_cards.append(x.dict())
+
+ with db.atomic():
+ for batch in chunked(new_cards, 30):
+ PitchingCard.insert_many(batch).on_conflict_replace().execute()
+
+ db.close()
+ return f'Updated cards: {updates}; new cards: {len(new_cards)}'
+
+
+@router.patch('/{card_id}')
+async def patch_card(
+ card_id: int, balk: Optional[int] = None, wild_pitch: Optional[int] = None, hold: Optional[int] = None,
+ starter_rating: Optional[int] = None, relief_rating: Optional[int] = None, closer_rating: Optional[int] = None,
+ batting: Optional[int] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch pitching cards. This event has been logged.'
+ )
+
+ this_card = PitchingCard.get_or_none(PitchingCard.id == card_id)
+ if this_card is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'PitchingCard id {card_id} not found')
+
+ if balk is not None:
+ this_card.balk = balk
+ if wild_pitch is not None:
+ this_card.wild_pitch = wild_pitch
+ if hold is not None:
+ this_card.hold = hold
+ if starter_rating is not None:
+ this_card.starter_rating = starter_rating
+ if relief_rating is not None:
+ this_card.relief_rating = relief_rating
+ if closer_rating is not None:
+ this_card.closer_rating = closer_rating
+ if batting is not None:
+ this_card.batting = batting
+
+ if this_card.save() == 1:
+ return_val = model_to_dict(this_card)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that card'
+ )
+
+
+@router.delete('/{card_id}')
+async def delete_card(card_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete pitching cards. This event has been logged.'
+ )
+
+ this_card = PitchingCard.get_or_none(PitchingCard.id == card_id)
+ if this_card is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Pitching id {card_id} not found')
+
+ count = this_card.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Card {this_card} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Card {this_card} could not be deleted')
+
+
+@router.delete('')
+async def delete_all_cards(token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete pitching cards. This event has been logged.'
+ )
+
+ d_query = PitchingCard.delete()
+ d_query.execute()
+
+ return f'Deleted {d_query.count()} pitching cards'
diff --git a/app/routers_v2/pitstats.py b/app/routers_v2/pitstats.py
new file mode 100644
index 0000000..29c4d3e
--- /dev/null
+++ b/app/routers_v2/pitstats.py
@@ -0,0 +1,191 @@
+from datetime import datetime
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional, List
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, PitchingStat, model_to_dict, Card, Player, Current
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/pitstats',
+ tags=['pitstats']
+)
+
+
+class PitStat(pydantic.BaseModel):
+ card_id: int
+ team_id: int
+ vs_team_id: int
+ roster_num: int
+ ip: float
+ hit: Optional[int] = 0
+ run: Optional[int] = 0
+ erun: Optional[int] = 0
+ so: Optional[int] = 0
+ bb: Optional[int] = 0
+ hbp: Optional[int] = 0
+ wp: Optional[int] = 0
+ balk: Optional[int] = 0
+ hr: Optional[int] = 0
+ ir: Optional[int] = 0
+ irs: Optional[int] = 0
+ gs: Optional[int] = 0
+ win: Optional[int] = 0
+ loss: Optional[int] = 0
+ hold: Optional[int] = 0
+ sv: Optional[int] = 0
+ bsv: Optional[int] = 0
+ week: int
+ season: int
+ created: Optional[int] = int(datetime.timestamp(datetime.now())*100000)
+ game_id: int
+
+
+class PitchingStatModel(pydantic.BaseModel):
+ stats: List[PitStat]
+
+
+@router.get('')
+async def get_pit_stats(
+ card_id: int = None, player_id: int = None, team_id: int = None, vs_team_id: int = None, week: int = None,
+ season: int = None, week_start: int = None, week_end: int = None, created: int = None, gs: bool = None,
+ csv: bool = None):
+ all_stats = PitchingStat.select().join(Card).join(Player)
+ logging.debug(f'pit query:\n\n{all_stats}')
+
+ if season is not None:
+ all_stats = all_stats.where(PitchingStat.season == season)
+ else:
+ curr = Current.latest()
+ all_stats = all_stats.where(PitchingStat.season == curr.season)
+
+ if card_id is not None:
+ all_stats = all_stats.where(PitchingStat.card_id == card_id)
+ if player_id is not None:
+ all_stats = all_stats.where(PitchingStat.card.player.player_id == player_id)
+ if team_id is not None:
+ all_stats = all_stats.where(PitchingStat.team_id == team_id)
+ if vs_team_id is not None:
+ all_stats = all_stats.where(PitchingStat.vs_team_id == vs_team_id)
+ if week is not None:
+ all_stats = all_stats.where(PitchingStat.week == week)
+ if week_start is not None:
+ all_stats = all_stats.where(PitchingStat.week >= week_start)
+ if week_end is not None:
+ all_stats = all_stats.where(PitchingStat.week <= week_end)
+ if created is not None:
+ all_stats = all_stats.where(PitchingStat.created <= created)
+ if gs is not None:
+ all_stats = all_stats.where(PitchingStat.gs == 1 if gs else 0)
+
+ # if all_stats.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No pitching stats found')
+
+ if csv:
+ data_list = [['id', 'card_id', 'player_id', 'cardset', 'team', 'vs_team', 'ip', 'hit', 'run', 'erun', 'so', 'bb', 'hbp',
+ 'wp', 'balk', 'hr', 'ir', 'irs', 'gs', 'win', 'loss', 'hold', 'sv', 'bsv', 'week', 'season',
+ 'created', 'game_id', 'roster_num']]
+ for line in all_stats:
+ data_list.append(
+ [
+ line.id, line.card.id, line.card.player.player_id, line.card.player.cardset.name, line.team.abbrev,
+ line.vs_team.abbrev, line.ip, line.hit,
+ line.run, line.erun, line.so, line.bb, line.hbp, line.wp, line.balk, line.hr, line.ir, line.irs,
+ line.gs, line.win, line.loss, line.hold, line.sv, line.bsv, line.week, line.season, line.created,
+ line.game_id, line.roster_num
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_stats.count(), 'stats': []}
+ for x in all_stats:
+ return_val['stats'].append(model_to_dict(x, recurse=False))
+
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_pitstat(stats: PitchingStatModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post stats. This event has been logged.'
+ )
+
+ new_stats = []
+ for x in stats.stats:
+ this_stat = PitchingStat(
+ card_id=x.card_id,
+ team_id=x.team_id,
+ vs_team_id=x.vs_team_id,
+ roster_num=x.roster_num,
+ ip=x.ip,
+ hit=x.hit,
+ run=x.run,
+ erun=x.erun,
+ so=x.so,
+ bb=x.bb,
+ hbp=x.hbp,
+ wp=x.wp,
+ balk=x.balk,
+ hr=x.hr,
+ ir=x.ir,
+ irs=x.irs,
+ gs=x.gs,
+ win=x.win,
+ loss=x.loss,
+ hold=x.hold,
+ sv=x.sv,
+ bsv=x.bsv,
+ week=x.week,
+ season=x.season,
+ created=x.created,
+ game_id=x.game_id
+ )
+ new_stats.append(this_stat)
+
+ with db.atomic():
+ PitchingStat.bulk_create(new_stats, batch_size=15)
+ db.close()
+
+ raise HTTPException(status_code=200, detail=f'{len(new_stats)} pitching lines have been added')
+
+
+@router.delete('/{stat_id}')
+async def delete_pitstat(stat_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete stats. This event has been logged.'
+ )
+ try:
+ this_stat = PitchingStat.get_by_id(stat_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No stat found with id {stat_id}')
+
+ count = this_stat.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Stat {stat_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Stat {stat_id} was not deleted')
diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py
new file mode 100644
index 0000000..9bddd15
--- /dev/null
+++ b/app/routers_v2/players.py
@@ -0,0 +1,708 @@
+import os.path
+import base64
+
+import pandas as pd
+from fastapi import APIRouter, Depends, HTTPException, Request, Response, Query
+from fastapi.responses import FileResponse
+from fastapi.templating import Jinja2Templates
+from html2image import Html2Image
+from typing import Optional, List, Literal
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..card_creation import get_batter_card_data, get_pitcher_card_data
+from ..db_engine import db, Player, model_to_dict, fn, chunked, Paperdex, Cardset, Rarity, BattingCard, \
+ BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/players',
+ tags=['players']
+)
+
+
+templates = Jinja2Templates(directory="storage/templates")
+
+
+class PlayerPydantic(pydantic.BaseModel):
+ player_id: int = None
+ p_name: str
+ cost: int
+ image: str
+ image2: Optional[str] = None
+ mlbclub: str
+ franchise: str
+ cardset_id: int
+ set_num: int
+ rarity_id: int
+ pos_1: str
+ pos_2: Optional[str] = None
+ pos_3: Optional[str] = None
+ pos_4: Optional[str] = None
+ pos_5: Optional[str] = None
+ pos_6: Optional[str] = None
+ pos_7: Optional[str] = None
+ pos_8: Optional[str] = None
+ headshot: Optional[str] = None
+ vanity_card: Optional[str] = None
+ strat_code: Optional[str] = None
+ bbref_id: Optional[str] = None
+ fangr_id: Optional[str] = None
+ description: str
+ quantity: Optional[int] = 999
+
+
+class PlayerModel(pydantic.BaseModel):
+ players: List[PlayerPydantic]
+
+
+@router.get('')
+async def get_players(
+ name: Optional[str] = None, value: Optional[int] = None, min_cost: Optional[int] = None,
+ max_cost: Optional[int] = None, has_image2: Optional[bool] = None, mlbclub: Optional[str] = None,
+ franchise: Optional[str] = None, cardset_id: list = Query(default=None), rarity_id: list = Query(default=None),
+ pos_include: list = Query(default=None), pos_exclude: list = Query(default=None), has_headshot: Optional[bool] = None,
+ has_vanity_card: Optional[bool] = None, strat_code: Optional[str] = None, bbref_id: Optional[str] = None,
+ fangr_id: Optional[str] = None, inc_dex: Optional[bool] = True, in_desc: Optional[str] = None,
+ flat: Optional[bool] = False, sort_by: Optional[str] = False, cardset_id_exclude: list = Query(default=None),
+ limit: Optional[int] = None, csv: Optional[bool] = None, short_output: Optional[bool] = False):
+ all_players = Player.select()
+ if all_players.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no players to filter')
+
+ if name is not None:
+ all_players = all_players.where(fn.Lower(Player.p_name) == name.lower())
+ if value is not None:
+ all_players = all_players.where(Player.cost == value)
+ if min_cost is not None:
+ all_players = all_players.where(Player.cost >= min_cost)
+ if max_cost is not None:
+ all_players = all_players.where(Player.cost <= max_cost)
+ if has_image2 is not None:
+ all_players = all_players.where(Player.image2.is_null(not has_image2))
+ if mlbclub is not None:
+ all_players = all_players.where(fn.Lower(Player.mlbclub) == mlbclub.lower())
+ if franchise is not None:
+ all_players = all_players.where(fn.Lower(Player.franchise) == franchise.lower())
+ if cardset_id is not None:
+ all_players = all_players.where(Player.cardset_id << cardset_id)
+ if cardset_id_exclude is not None:
+ all_players = all_players.where(Player.cardset_id.not_in(cardset_id_exclude))
+ if rarity_id is not None:
+ all_players = all_players.where(Player.rarity_id << rarity_id)
+ if pos_include is not None:
+ p_list = [x.upper() for x in pos_include]
+ all_players = all_players.where(
+ (Player.pos_1 << p_list) | (Player.pos_2 << p_list) | (Player.pos_3 << p_list) | (Player.pos_4 << p_list) |
+ (Player.pos_5 << p_list) | (Player.pos_6 << p_list) | (Player.pos_7 << p_list) | (Player.pos_8 << p_list)
+ )
+ if has_headshot is not None:
+ all_players = all_players.where(Player.headshot.is_null(not has_headshot))
+ if has_vanity_card is not None:
+ all_players = all_players.where(Player.vanity_card.is_null(not has_vanity_card))
+ if strat_code is not None:
+ all_players = all_players.where(Player.strat_code == strat_code)
+ if bbref_id is not None:
+ all_players = all_players.where(Player.bbref_id == bbref_id)
+ if fangr_id is not None:
+ all_players = all_players.where(Player.fangr_id == fangr_id)
+ if in_desc is not None:
+ all_players = all_players.where(fn.Lower(Player.description).contains(in_desc.lower()))
+
+ if sort_by is not None:
+ if sort_by == 'cost-desc':
+ all_players = all_players.order_by(-Player.cost)
+ elif sort_by == 'cost-asc':
+ all_players = all_players.order_by(Player.cost)
+ elif sort_by == 'name-asc':
+ all_players = all_players.order_by(Player.p_name)
+ elif sort_by == 'name-desc':
+ all_players = all_players.order_by(-Player.p_name)
+ elif sort_by == 'rarity-desc':
+ all_players = all_players.order_by(Player.rarity)
+ elif sort_by == 'rarity-asc':
+ all_players = all_players.order_by(-Player.rarity)
+
+ final_players = []
+ # logging.info(f'pos_exclude: {type(pos_exclude)} - {pos_exclude} - is None: {pos_exclude is None}')
+ for x in all_players:
+ if pos_exclude is not None and set([x.upper() for x in pos_exclude]).intersection(x.get_all_pos()):
+ pass
+ else:
+ final_players.append(x)
+
+ if limit is not None and len(final_players) >= limit:
+ break
+
+ # if len(final_players) == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No players found')
+
+ if csv:
+ card_vals = [model_to_dict(x) for x in all_players]
+ db.close()
+
+ for x in card_vals:
+ x['player_name'] = x['p_name']
+ x['cardset_name'] = x['cardset']['name']
+ x['rarity'] = x['rarity']['name']
+ x['for_purchase'] = x['cardset']['for_purchase']
+ x['ranked_legal'] = x['cardset']['ranked_legal']
+ if x['player_name'] not in x['description']:
+ x['description'] = f'{x["description"]} {x["player_name"]}'
+
+ card_df = pd.DataFrame(card_vals)
+ output = card_df[[
+ 'player_id', 'player_name', 'cost', 'image', 'image2', 'mlbclub', 'franchise', 'cardset_name', 'rarity',
+ 'pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card',
+ 'fangr_id', 'bbref_id', 'description', 'for_purchase', 'ranked_legal'
+ ]]
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+ # all_players.order_by(-Player.rarity.value, Player.p_name)
+ # data_list = [['id', 'name', 'value', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1',
+ # 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card',
+ # 'strat_code', 'bbref_id', 'description', 'for_purchase', 'ranked_legal']]
+ # for line in final_players:
+ # data_list.append(
+ # [
+ # line.player_id, line.p_name, line.cost, line.image, line.image2, line.mlbclub, line.franchise,
+ # line.cardset, line.rarity, line.pos_1, line.pos_2, line.pos_3, line.pos_4, line.pos_5, line.pos_6,
+ # line.pos_7, line.pos_8, line.headshot, line.vanity_card, line.strat_code, line.bbref_id,
+ # line.description, line.cardset.for_purchase, line.cardset.ranked_legal
+ # # line.description, line.cardset.in_packs, line.quantity
+ # ]
+ # )
+ # return_val = DataFrame(data_list).to_csv(header=False, index=False)
+ #
+ # db.close()
+ # return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': len(final_players), 'players': []}
+ for x in final_players:
+
+ this_record = model_to_dict(x, recurse=not (flat or short_output))
+
+ if inc_dex:
+ this_dex = Paperdex.select().where(Paperdex.player == x)
+ this_record['paperdex'] = {'count': this_dex.count(), 'paperdex': []}
+ for y in this_dex:
+ this_record['paperdex']['paperdex'].append(model_to_dict(y, recurse=False))
+
+ return_val['players'].append(this_record)
+
+ # return_val['players'].append(model_to_dict(x, recurse=not flat))
+
+ db.close()
+ return return_val
+
+
+@router.get('/random')
+async def get_random_player(
+ min_cost: Optional[int] = None, max_cost: Optional[int] = None, in_packs: Optional[bool] = None,
+ min_rarity: Optional[int] = None, max_rarity: Optional[int] = None, limit: Optional[int] = None,
+ pos_include: Optional[str] = None, pos_exclude: Optional[str] = None, franchise: Optional[str] = None,
+ mlbclub: Optional[str] = None, cardset_id: list = Query(default=None), pos_inc: list = Query(default=None),
+ pos_exc: list = Query(default=None), csv: Optional[bool] = None):
+ all_players = (Player
+ .select()
+ .join(Cardset)
+ .switch(Player)
+ .join(Rarity)
+ .order_by(fn.Random()))
+
+ if min_cost is not None:
+ all_players = all_players.where(Player.cost >= min_cost)
+ if max_cost is not None:
+ all_players = all_players.where(Player.cost <= max_cost)
+ if in_packs is not None:
+ if in_packs:
+ all_players = all_players.where(Player.cardset.in_packs)
+ if min_rarity is not None:
+ all_players = all_players.where(Player.rarity.value >= min_rarity)
+ if max_rarity is not None:
+ all_players = all_players.where(Player.rarity.value <= max_rarity)
+ if pos_include is not None:
+ all_players = all_players.where(
+ (fn.lower(Player.pos_1) == pos_include.lower()) | (fn.lower(Player.pos_2) == pos_include.lower()) |
+ (fn.lower(Player.pos_3) == pos_include.lower()) | (fn.lower(Player.pos_4) == pos_include.lower()) |
+ (fn.lower(Player.pos_5) == pos_include.lower()) | (fn.lower(Player.pos_6) == pos_include.lower()) |
+ (fn.lower(Player.pos_7) == pos_include.lower()) | (fn.lower(Player.pos_8) == pos_include.lower())
+ )
+ if franchise is not None:
+ all_players = all_players.where(fn.Lower(Player.franchise) == franchise.lower())
+ if mlbclub is not None:
+ all_players = all_players.where(fn.Lower(Player.mlbclub) == mlbclub.lower())
+ if cardset_id is not None:
+ all_players = all_players.where(Player.cardset_id << cardset_id)
+ if pos_inc is not None:
+ p_list = [x.upper() for x in pos_inc]
+ all_players = all_players.where(
+ (Player.pos_1 << p_list) | (Player.pos_2 << p_list) | (Player.pos_3 << p_list) | (Player.pos_4 << p_list) |
+ (Player.pos_5 << p_list) | (Player.pos_6 << p_list) | (Player.pos_7 << p_list) | (Player.pos_8 << p_list)
+ )
+ # if pos_exc is not None:
+ # p_list = [x.upper() for x in pos_exc]
+ # logging.info(f'starting query: {all_players}\n\np_list: {p_list}\n\n')
+ # all_players = all_players.where(
+ # Player.pos_1.not_in(p_list) & Player.pos_2.not_in(p_list) & Player.pos_3.not_in(p_list) &
+ # Player.pos_4.not_in(p_list) & Player.pos_5.not_in(p_list) & Player.pos_6.not_in(p_list) &
+ # Player.pos_7.not_in(p_list) & Player.pos_8.not_in(p_list)
+ # )
+ # logging.info(f'post pos query: {all_players}')
+
+ if pos_exclude is not None and pos_exc is None:
+ final_players = [x for x in all_players if pos_exclude not in x.get_all_pos()]
+ elif pos_exc is not None and pos_exclude is None:
+ final_players = []
+ p_list = [x.upper() for x in pos_exc]
+ for x in all_players:
+ if limit is not None and len(final_players) >= limit:
+ break
+ if not set(p_list).intersection(x.get_all_pos()):
+ final_players.append(x)
+ else:
+ final_players = all_players
+
+ if limit is not None:
+ final_players = final_players[:limit]
+
+ # if len(final_players) == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'No players found')
+
+ if csv:
+ data_list = [['id', 'name', 'cost', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1',
+ 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card',
+ 'strat_code', 'bbref_id', 'description']]
+ for line in final_players:
+ data_list.append(
+ [
+ line.id, line.p_name, line.cost, line.image, line.image2,
+ line.mlbclub, line.franchise, line.cardset.name, line.rarity.name,
+ line.pos_1, line.pos_2, line.pos_3, line.pos_4, line.pos_5,
+ line.pos_6, line.pos_7, line.pos_8, line.headshot, line.vanity_card,
+ line.strat_code, line.bbref_id, line.description
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': len(final_players), 'players': []}
+ for x in final_players:
+ this_record = model_to_dict(x)
+
+ this_dex = Paperdex.select().where(Paperdex.player == x)
+ this_record['paperdex'] = {'count': this_dex.count(), 'paperdex': []}
+ for y in this_dex:
+ this_record['paperdex']['paperdex'].append(model_to_dict(y, recurse=False))
+
+ return_val['players'].append(this_record)
+ # return_val['players'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{player_id}')
+async def get_one_player(player_id, csv: Optional[bool] = False):
+ try:
+ this_player = Player.get_by_id(player_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No player found with id {player_id}')
+
+ if csv:
+ data_list = [['id', 'name', 'cost', 'image', 'image2', 'mlbclub', 'franchise', 'cardset', 'rarity', 'pos_1',
+ 'pos_2', 'pos_3', 'pos_4', 'pos_5', 'pos_6', 'pos_7', 'pos_8', 'headshot', 'vanity_card',
+ 'strat_code', 'bbref_id', 'description']]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+ data_list.append(
+ [
+ this_player.id, this_player.p_name, this_player.cost, this_player.image, this_player.image2,
+ this_player.mlbclub, this_player.franchise, this_player.cardset.name, this_player.rarity.name,
+ this_player.pos_1, this_player.pos_2, this_player.pos_3, this_player.pos_4, this_player.pos_5,
+ this_player.pos_6, this_player.pos_7, this_player.pos_8, this_player.headshot, this_player.vanity_card,
+ this_player.strat_code, this_player.bbref_id, this_player.description
+ ]
+ )
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+ else:
+ return_val = model_to_dict(this_player)
+ this_dex = Paperdex.select().where(Paperdex.player == this_player)
+ return_val['paperdex'] = {'count': this_dex.count(), 'paperdex': []}
+ for x in this_dex:
+ return_val['paperdex']['paperdex'].append(model_to_dict(x, recurse=False))
+ db.close()
+ return return_val
+
+
+@router.get('/{player_id}/{card_type}card')
+async def get_batter_card(
+ request: Request, player_id: int, card_type: Literal['batting', 'pitching'], variant: int = 0, d: str = None,
+ html: Optional[bool] = False):
+ try:
+ this_player = Player.get_by_id(player_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No player found with id {player_id}')
+
+ if os.path.isfile(f'storage/cards/cardset-{this_player.cardset.id}/{player_id}-{d}-v{variant}.png') and html is False:
+ db.close()
+ return FileResponse(
+ path=f'storage/cards/cardset-{this_player.cardset.id}/{player_id}-{d}-v{variant}.png',
+ media_type='image/png'
+ )
+
+ all_pos = CardPosition.select().where(CardPosition.player == this_player).order_by(CardPosition.innings.desc())
+
+ if card_type == 'batting':
+ this_bc = BattingCard.get_or_none(BattingCard.player == this_player, BattingCard.variant == variant)
+ if this_bc is None:
+ raise HTTPException(status_code=404, detail=f'Batting card not found for id {player_id}, variant {variant}')
+
+ rating_vl = BattingCardRatings.get_or_none(
+ BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'L')
+ rating_vr = BattingCardRatings.get_or_none(
+ BattingCardRatings.battingcard == this_bc, BattingCardRatings.vs_hand == 'R')
+ if None in [rating_vr, rating_vl]:
+ raise HTTPException(status_code=404, detail=f'Ratings not found for batting card {this_bc.id}')
+
+ card_data = get_batter_card_data(this_player, this_bc, rating_vl, rating_vr, all_pos)
+ if this_player.description in this_player.cardset.name:
+ card_data['cardset_name'] = this_player.cardset.name
+ else:
+ card_data['cardset_name'] = this_player.description
+ card_data['request'] = request
+ html_response = templates.TemplateResponse("player_card.html", card_data)
+
+ else:
+ this_pc = PitchingCard.get_or_none(PitchingCard.player == this_player, PitchingCard.variant == variant)
+ if this_pc is None:
+ raise HTTPException(
+ status_code=404, detail=f'Pitching card not found for id {player_id}, variant {variant}')
+
+ rating_vl = PitchingCardRatings.get_or_none(
+ PitchingCardRatings.pitchingcard == this_pc, PitchingCardRatings.vs_hand == 'L')
+ rating_vr = PitchingCardRatings.get_or_none(
+ PitchingCardRatings.pitchingcard == this_pc, PitchingCardRatings.vs_hand == 'R')
+ if None in [rating_vr, rating_vl]:
+ raise HTTPException(status_code=404, detail=f'Ratings not found for pitching card {this_pc.id}')
+
+ card_data = get_pitcher_card_data(this_player, this_pc, rating_vl, rating_vr, all_pos)
+ if this_player.description in this_player.cardset.name:
+ card_data['cardset_name'] = this_player.cardset.name
+ else:
+ card_data['cardset_name'] = this_player.description
+ card_data['request'] = request
+ html_response = templates.TemplateResponse("player_card.html", card_data)
+
+ if html:
+ db.close()
+ return html_response
+
+ updates = 0
+ if card_type == 'batting':
+ updates += BattingCardRatings.update(card_data['new_ratings_vl'].dict()).where(
+ (BattingCardRatings.id == rating_vl.id)
+ ).execute()
+ updates += BattingCardRatings.update(card_data['new_ratings_vr'].dict()).where(
+ (BattingCardRatings.id == rating_vr.id)
+ ).execute()
+ else:
+ updates += PitchingCardRatings.update(card_data['new_ratings_vl'].dict()).where(
+ (PitchingCardRatings.id == rating_vl.id)
+ ).execute()
+ updates += PitchingCardRatings.update(card_data['new_ratings_vr'].dict()).where(
+ (PitchingCardRatings.id == rating_vr.id)
+ ).execute()
+
+ logging.info(f'Rating updates: {updates}')
+
+ hti = Html2Image(
+ browser='chromium',
+ size=(1200, 600),
+ output_path=f'storage/cards/cardset-{this_player.cardset.id}/',
+ custom_flags=['--no-sandbox', '--disable-remote-debugging', '--headless', '--disable-gpu',
+ '--disable-software-rasterizer', '--disable-dev-shm-usage']
+ )
+
+ logging.debug(f'body:\n{html_response.body.decode("UTF-8")}')
+ x = hti.screenshot(
+ html_str=str(html_response.body.decode("UTF-8")),
+ save_as=f'{player_id}-{d}-v{variant}.png'
+ )
+
+ db.close()
+ return FileResponse(path=x[0], media_type='image/png')
+
+
+# @router.get('/{player_id}/pitchingcard')
+# async def get_pitcher_card(
+# request: Request, player_id: int, variant: int = 0, d: str = None, html: Optional[bool] = False)
+
+
+@router.patch('/{player_id}')
+async def v1_players_patch(
+ player_id, name: Optional[str] = None, image: Optional[str] = None, image2: Optional[str] = None,
+ mlbclub: Optional[str] = None, franchise: Optional[str] = None, cardset_id: Optional[int] = None,
+ rarity_id: Optional[int] = None, pos_1: Optional[str] = None, pos_2: Optional[str] = None,
+ pos_3: Optional[str] = None, pos_4: Optional[str] = None, pos_5: Optional[str] = None,
+ pos_6: Optional[str] = None, pos_7: Optional[str] = None, pos_8: Optional[str] = None,
+ headshot: Optional[str] = None, vanity_card: Optional[str] = None, strat_code: Optional[str] = None,
+ bbref_id: Optional[str] = None, description: Optional[str] = None, cost: Optional[int] = None,
+ fangr_id: Optional[str] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch players. This event has been logged.'
+ )
+
+ try:
+ this_player = Player.get_by_id(player_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No player found with id {player_id}')
+
+ if cost is not None:
+ this_player.cost = cost
+ if name is not None:
+ this_player.p_name = name
+ if image is not None:
+ this_player.image = image
+ if image2 is not None:
+ if image2.lower() == 'false':
+ this_player.image2 = None
+ else:
+ this_player.image2 = image2
+ if mlbclub is not None:
+ this_player.mlbclub = mlbclub
+ if franchise is not None:
+ this_player.franchise = franchise
+ if cardset_id is not None:
+ try:
+ this_cardset = Cardset.get_by_id(cardset_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cardset found with id {cardset_id}')
+ this_player.cardset = this_cardset
+ if rarity_id is not None:
+ try:
+ this_rarity = Rarity.get_by_id(rarity_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
+ this_player.rarity = this_rarity
+ if pos_1 is not None:
+ if pos_1 in ['None', 'False', '']:
+ this_player.pos_1 = None
+ else:
+ this_player.pos_1 = pos_1
+ if pos_2 is not None:
+ if pos_2 in ['None', 'False', '']:
+ this_player.pos_2 = None
+ else:
+ this_player.pos_2 = pos_2
+ if pos_3 is not None:
+ if pos_3 in ['None', 'False', '']:
+ this_player.pos_3 = None
+ else:
+ this_player.pos_3 = pos_3
+ if pos_4 is not None:
+ if pos_4 in ['None', 'False', '']:
+ this_player.pos_4 = None
+ else:
+ this_player.pos_4 = pos_4
+ if pos_5 is not None:
+ if pos_5 in ['None', 'False', '']:
+ this_player.pos_5 = None
+ else:
+ this_player.pos_5 = pos_5
+ if pos_6 is not None:
+ if pos_6 in ['None', 'False', '']:
+ this_player.pos_6 = None
+ else:
+ this_player.pos_6 = pos_6
+ if pos_7 is not None:
+ if pos_7 in ['None', 'False', '']:
+ this_player.pos_7 = None
+ else:
+ this_player.pos_7 = pos_7
+ if pos_8 is not None:
+ if pos_8 in ['None', 'False', '']:
+ this_player.pos_8 = None
+ else:
+ this_player.pos_8 = pos_8
+ if headshot is not None:
+ this_player.headshot = headshot
+ if vanity_card is not None:
+ this_player.vanity_card = vanity_card
+ if strat_code is not None:
+ this_player.strat_code = strat_code
+ if bbref_id is not None:
+ this_player.bbref_id = bbref_id
+ if fangr_id is not None:
+ this_player.fangr_id = fangr_id
+ if description is not None:
+ this_player.description = description
+
+ if this_player.save() == 1:
+ return_val = model_to_dict(this_player)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.put('')
+async def put_players(players: PlayerModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post players. This event has been logged.'
+ )
+
+ new_players = []
+ for x in players.players:
+ # this_player = Player(
+ # player_id=x.player_id,
+ # p_name=x.p_name,
+ # cost=x.cost,
+ # image=x.image,
+ # image2=x.image2,
+ # mlbclub=x.mlbclub,
+ # franchise=x.franchise,
+ # cardset_id=x.cardset_id,
+ # rarity_id=x.rarity_id,
+ # set_num=x.set_num,
+ # pos_1=x.pos_1,
+ # pos_2=x.pos_2,
+ # pos_3=x.pos_3,
+ # pos_4=x.pos_4,
+ # pos_5=x.pos_5,
+ # pos_6=x.pos_6,
+ # pos_7=x.pos_7,
+ # pos_8=x.pos_8,
+ # headshot=x.headshot,
+ # vanity_card=x.vanity_card,
+ # strat_code=x.strat_code,
+ # fangr_id=x.fangr_id,
+ # bbref_id=x.bbref_id,
+ # description=x.description
+ # )
+ # new_players.append(this_player)
+ new_players.append({
+ 'player_id': x.player_id,
+ 'p_name': x.p_name,
+ 'cost': x.cost,
+ 'image': x.image,
+ 'image2': x.image2,
+ 'mlbclub': x.mlbclub.title(),
+ 'franchise': x.franchise.title(),
+ 'cardset_id': x.cardset_id,
+ 'rarity_id': x.rarity_id,
+ 'set_num': x.set_num,
+ 'pos_1': x.pos_1,
+ 'pos_2': x.pos_2,
+ 'pos_3': x.pos_3,
+ 'pos_4': x.pos_4,
+ 'pos_5': x.pos_5,
+ 'pos_6': x.pos_6,
+ 'pos_7': x.pos_7,
+ 'pos_8': x.pos_8,
+ 'headshot': x.headshot,
+ 'vanity_card': x.vanity_card,
+ 'strat_code': x.strat_code,
+ 'fangr_id': x.fangr_id,
+ 'bbref_id': x.bbref_id,
+ 'description': x.description
+ })
+
+ logging.info(f'new_players: {new_players}')
+
+ with db.atomic():
+ # Player.bulk_create(new_players, batch_size=15)
+ for batch in chunked(new_players, 15):
+ logging.info(f'batch: {batch}')
+ Player.insert_many(batch).on_conflict_replace().execute()
+ db.close()
+
+ # sheets.update_all_players(SHEETS_AUTH)
+ raise HTTPException(status_code=200, detail=f'{len(new_players)} players have been added')
+
+
+@router.post('')
+async def post_players(new_player: PlayerPydantic, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post players. This event has been logged.'
+ )
+
+ dupe_query = Player.select().where(
+ (Player.bbref_id == new_player.bbref_id) & (Player.cardset_id == new_player.cardset_id)
+ )
+ if dupe_query.count() != 0:
+ db.close()
+ raise HTTPException(
+ status_code=400,
+ detail=f'This appears to be a duplicate with player {dupe_query[0].player_id}'
+ )
+
+ p_query = Player.select(Player.player_id).order_by(-Player.player_id).limit(1)
+ new_id = p_query[0].player_id + 1
+
+ new_player.player_id = new_id
+ p_id = Player.insert(new_player.dict()).execute()
+
+ return_val = model_to_dict(Player.get_by_id(p_id))
+ db.close()
+ return return_val
+
+
+@router.delete('/{player_id}')
+async def delete_player(player_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete players. This event has been logged.'
+ )
+
+ try:
+ this_player = Player.get_by_id(player_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No player found with id {player_id}')
+
+ count = this_player.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Player {player_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Player {player_id} was not deleted')
diff --git a/app/routers_v2/rarity.py b/app/routers_v2/rarity.py
new file mode 100644
index 0000000..6078e29
--- /dev/null
+++ b/app/routers_v2/rarity.py
@@ -0,0 +1,187 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Rarity, model_to_dict, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/rarities',
+ tags=['rarities']
+)
+
+
+class RarityModel(pydantic.BaseModel):
+ value: int
+ name: str
+ color: str
+
+
+@router.get('')
+async def get_rarities(value: Optional[int] = None, name: Optional[str] = None, min_value: Optional[int] = None,
+ max_value: Optional[int] = None, csv: Optional[bool] = None):
+ all_rarities = Rarity.select()
+
+ if all_rarities.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no rarities to filter')
+
+ if value is not None:
+ all_rarities = all_rarities.where(Rarity.value == value)
+ if name is not None:
+ all_rarities = all_rarities.where(fn.Lower(Rarity.name) == name.lower())
+ if min_value is not None:
+ all_rarities = all_rarities.where(Rarity.value >= min_value)
+ if max_value is not None:
+ all_rarities = all_rarities.where(Rarity.value <= max_value)
+
+ if all_rarities.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No rarities found')
+
+ if csv:
+ data_list = [['id', 'value', 'name']]
+ for line in all_rarities:
+ data_list.append(
+ [
+ line.id, line.value, line.name
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_rarities.count(), 'rarities': []}
+ for x in all_rarities:
+ return_val['rarities'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{rarity_id}')
+async def get_one_rarity(rarity_id, csv: Optional[bool] = False):
+ try:
+ this_rarity = Rarity.get_by_id(rarity_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
+
+ if csv:
+ data_list = [['id', 'value', 'name']]
+ for line in this_rarity:
+ data_list.append(
+ [
+ line.id, line.value, line.name
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+ else:
+ return_val = model_to_dict(this_rarity)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_rarity(rarity: RarityModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post rarities. This event has been logged.'
+ )
+
+ dupe_team = Rarity.get_or_none(Rarity.name)
+ if dupe_team:
+ db.close()
+ raise HTTPException(status_code=400, detail=f'There is already a rarity using {rarity.name}')
+
+ this_rarity = Rarity(
+ value=rarity.value,
+ name=rarity.name,
+ color=rarity.color
+ )
+
+ saved = this_rarity.save()
+ if saved == 1:
+ return_val = model_to_dict(this_rarity)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.patch('/{rarity_id}')
+async def patch_rarity(
+ rarity_id, value: Optional[int] = None, name: Optional[str] = None, color: Optional[str] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch rarities. This event has been logged.'
+ )
+ try:
+ this_rarity = Rarity.get_by_id(rarity_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
+
+ if value is not None:
+ this_rarity.value = value
+ if name is not None:
+ this_rarity.name = name
+ if color is not None:
+ this_rarity.color = color
+
+ if this_rarity.save() == 1:
+ return_val = model_to_dict(this_rarity)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{rarity_id}')
+async def v1_rarities_delete(rarity_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete rarities. This event has been logged.'
+ )
+ try:
+ this_rarity = Rarity.get_by_id(rarity_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No rarity found with id {rarity_id}')
+
+ count = this_rarity.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Rarity {rarity_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Rarity {rarity_id} was not deleted')
diff --git a/app/routers_v2/results.py b/app/routers_v2/results.py
new file mode 100644
index 0000000..557e04e
--- /dev/null
+++ b/app/routers_v2/results.py
@@ -0,0 +1,431 @@
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Result, model_to_dict, Team, DataError
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/results',
+ tags=['results']
+)
+
+
+class ResultModel(pydantic.BaseModel):
+ away_team_id: int
+ home_team_id: int
+ away_score: int
+ home_score: int
+ away_team_value: Optional[int] = None
+ home_team_value: Optional[int] = None
+ away_team_ranking: Optional[int] = None
+ home_team_ranking: Optional[int] = None
+ scorecard: str
+ week: int
+ season: int
+ ranked: bool
+ short_game: bool
+ game_type: str
+
+
+@router.get('')
+async def get_results(
+ away_team_id: Optional[int] = None, home_team_id: Optional[int] = None, team_one_id: Optional[int] = None,
+ team_two_id: Optional[int] = None, away_score_min: Optional[int] = None, away_score_max: Optional[int] = None,
+ home_score_min: Optional[int] = None, home_score_max: Optional[int] = None, bothscore_min: Optional[int] = None,
+ bothscore_max: Optional[int] = None, season: Optional[int] = None, week: Optional[int] = None,
+ week_start: Optional[int] = None, week_end: Optional[int] = None, ranked: Optional[bool] = None,
+ short_game: Optional[bool] = None, game_type: Optional[str] = None, vs_ai: Optional[bool] = None,
+ csv: Optional[bool] = None):
+ all_results = Result.select()
+
+ # if all_results.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'There are no results to filter')
+
+ if away_team_id is not None:
+ try:
+ this_team = Team.get_by_id(away_team_id)
+ all_results = all_results.where(Result.away_team == this_team)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {away_team_id}')
+
+ if home_team_id is not None:
+ try:
+ this_team = Team.get_by_id(home_team_id)
+ all_results = all_results.where(Result.home_team == this_team)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {home_team_id}')
+
+ if team_one_id is not None:
+ try:
+ this_team = Team.get_by_id(team_one_id)
+ all_results = all_results.where((Result.home_team == this_team) | (Result.away_team == this_team))
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_one_id}')
+
+ if team_two_id is not None:
+ try:
+ this_team = Team.get_by_id(team_two_id)
+ all_results = all_results.where((Result.home_team == this_team) | (Result.away_team == this_team))
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_two_id}')
+
+ if away_score_min is not None:
+ all_results = all_results.where(Result.away_score >= away_score_min)
+
+ if away_score_max is not None:
+ all_results = all_results.where(Result.away_score <= away_score_max)
+
+ if home_score_min is not None:
+ all_results = all_results.where(Result.home_score >= home_score_min)
+
+ if home_score_max is not None:
+ all_results = all_results.where(Result.home_score <= home_score_max)
+
+ if bothscore_min is not None:
+ all_results = all_results.where((Result.home_score >= bothscore_min) & (Result.away_score >= bothscore_min))
+
+ if bothscore_max is not None:
+ all_results = all_results.where((Result.home_score <= bothscore_max) & (Result.away_score <= bothscore_max))
+
+ if season is not None:
+ all_results = all_results.where(Result.season == season)
+
+ if week is not None:
+ all_results = all_results.where(Result.week == week)
+
+ if ranked is not None:
+ all_results = all_results.where(Result.ranked == ranked)
+
+ if short_game is not None:
+ all_results = all_results.where(Result.short_game == short_game)
+
+ if week_start is not None:
+ all_results = all_results.where(Result.week >= week_start)
+
+ if week_end is not None:
+ all_results = all_results.where(Result.week <= week_end)
+
+ if game_type is not None:
+ all_results = all_results.where(Result.game_type == game_type)
+
+ all_results = all_results.order_by(Result.id)
+ # Not functional
+ # if vs_ai is not None:
+ # AwayTeam = Team.alias()
+ # all_results = all_results.join(
+ # Team, on=Result.home_team
+ # ).switch(Result).join(
+ # Team, on=(AwayTeam.id == Result.away_team).alias('a_team')
+ # )
+ #
+ # if vs_ai:
+ # all_results = all_results.where(
+ # (Result.home_team.is_ai == 1) | (Result.a_team.is_ai == 1)
+ # )
+ # else:
+ # all_results = all_results.where(
+ # (Result.home_team.is_ai == 0) & (Result.a_team.is_ai == 0)
+ # )
+ # logging.info(f'Result Query:\n\n{all_results}')
+
+ if csv:
+ data_list = [['id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_tv', 'home_tv',
+ 'game_type', 'season', 'week', 'short_game', 'ranked']]
+ for line in all_results:
+ data_list.append([
+ line.id, line.away_team.abbrev, line.home_team.abbrev, line.away_score, line.home_score,
+ line.away_team_value, line.home_team_value, line.game_type if line.game_type else 'minor-league',
+ line.season, line.week, line.short_game, line.ranked
+ ])
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_results.count(), 'results': []}
+ for x in all_results:
+ return_val['results'].append(model_to_dict(x))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{result_id}')
+async def get_one_results(result_id, csv: Optional[bool] = None):
+ try:
+ this_result = Result.get_by_id(result_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_tv', 'home_tv', 'game_type',
+ 'season', 'week', 'game_type'],
+ [this_result.id, this_result.away_team.abbrev, this_result.away_team.abbrev, this_result.away_score,
+ this_result.home_score, this_result.away_team_value, this_result.home_team_value,
+ this_result.game_type if this_result.game_type else 'minor-league',
+ this_result.season, this_result.week, this_result.game_type]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_result)
+ db.close()
+ return return_val
+
+
+@router.get('/team/{team_id}')
+async def get_team_results(
+ team_id: int, season: Optional[int] = None, week: Optional[int] = None, csv: Optional[bool] = False):
+ all_results = Result.select().where((Result.away_team_id == team_id) | (Result.home_team_id == team_id))
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception as e:
+ logging.error(f'Unknown team id {team_id} trying to pull team results')
+ raise HTTPException(404, f'Team id {team_id} not found')
+
+ if season is not None:
+ all_results = all_results.where(Result.season == season)
+ else:
+ all_results = all_results.where(Result.season == this_team.season)
+
+ if week is not None:
+ all_results = all_results.where(Result.week == week)
+
+ r_wins, r_loss, c_wins, c_loss = 0, 0, 0, 0
+ for x in all_results:
+ if x.away_team_id == team_id:
+ if x.away_score > x.home_score:
+ if x.ranked:
+ r_wins += 1
+ else:
+ c_wins += 1
+ else:
+ if x.ranked:
+ r_loss += 1
+ else:
+ c_loss += 1
+ elif x.home_team_id == team_id:
+ if x.away_score > x.home_score:
+ if x.ranked:
+ r_loss += 1
+ else:
+ c_loss += 1
+ else:
+ if x.ranked:
+ r_wins += 1
+ else:
+ c_wins += 1
+
+ if csv:
+ data_list = [
+ ['team_id', 'ranked_wins', 'ranked_losses', 'casual_wins', 'casual_losses', 'team_ranking'],
+ [team_id, r_wins, r_loss, c_wins, c_loss, this_team.ranking]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {
+ 'team': model_to_dict(this_team),
+ 'ranked_wins': r_wins,
+ 'ranked_losses': r_loss,
+ 'casual_wins': c_wins,
+ 'casual_losses': c_loss,
+ }
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_result(result: ResultModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post results. This event has been logged.'
+ )
+
+ this_result = Result(**result.__dict__)
+ saved = this_result.save()
+
+ if result.ranked:
+ if not result.away_team_ranking:
+ db.close()
+ error = f'Ranked game did not include away team ({result.away_team_id}) ranking.'
+ logging.error(error)
+ raise DataError(error)
+ if not result.home_team_ranking:
+ db.close()
+ error = f'Ranked game did not include home team ({result.home_team_id}) ranking.'
+ logging.error(error)
+ raise DataError(error)
+
+ k_value = 20 if result.short_game else 60
+ ratio = (result.home_team_ranking - result.away_team_ranking) / 400
+ exp_score = 1 / (1 + (10 ** ratio))
+ away_win = True if result.away_score > result.home_score else False
+ total_delta = k_value * exp_score
+ high_delta = total_delta * exp_score if exp_score > .5 else total_delta * (1 - exp_score)
+ low_delta = total_delta - high_delta
+
+ # exp_score > .5 means away team is favorite
+ if exp_score > .5 and away_win:
+ final_delta = low_delta
+ away_delta = low_delta * 3
+ home_delta = -low_delta
+ elif away_win:
+ final_delta = high_delta
+ away_delta = high_delta * 3
+ home_delta = -high_delta
+ elif exp_score <= .5 and not away_win:
+ final_delta = low_delta
+ away_delta = -low_delta
+ home_delta = low_delta * 3
+ elif not away_win:
+ final_delta = high_delta
+ away_delta = -high_delta
+ home_delta = high_delta * 3
+ else:
+ final_delta = 0
+ away_delta = 0
+ home_delta = 0
+
+ logging.debug(f'/results ranking deltas\n\nk_value: {k_value} / ratio: {ratio} / '
+ f'exp_score: {exp_score} / away_win: {away_win} / total_delta: {total_delta} / '
+ f'high_delta: {high_delta} / low_delta: {low_delta} / final_delta: {final_delta} / ')
+
+ away_team = Team.get_by_id(result.away_team_id)
+ away_team.ranking += away_delta
+ away_team.save()
+ logging.info(f'Just updated {away_team.abbrev} ranking to {away_team.ranking}')
+ home_team = Team.get_by_id(result.home_team_id)
+ home_team.ranking += home_delta
+ home_team.save()
+ logging.info(f'Just updated {home_team.abbrev} ranking to {home_team.ranking}')
+
+ if saved == 1:
+ return_val = model_to_dict(this_result)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that roster'
+ )
+
+
+@router.patch('/{result_id}')
+async def patch_result(
+ result_id, away_team_id: Optional[int] = None, home_team_id: Optional[int] = None,
+ away_score: Optional[int] = None, home_score: Optional[int] = None, away_team_value: Optional[int] = None,
+ home_team_value: Optional[int] = None, scorecard: Optional[str] = None, week: Optional[int] = None,
+ season: Optional[int] = None, short_game: Optional[bool] = None, game_type: Optional[str] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch results. This event has been logged.'
+ )
+ try:
+ this_result = Result.get_by_id(result_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
+
+ if away_team_id is not None:
+ this_result.away_team_id = away_team_id
+
+ if home_team_id is not None:
+ this_result.home_team_id = home_team_id
+
+ if away_score is not None:
+ this_result.away_score = away_score
+
+ if home_score is not None:
+ this_result.home_score = home_score
+
+ if away_team_value is not None:
+ this_result.away_team_value = away_team_value
+
+ if home_team_value is not None:
+ this_result.home_team_value = home_team_value
+
+ if scorecard is not None:
+ this_result.scorecard = scorecard
+
+ if week is not None:
+ this_result.week = week
+
+ if season is not None:
+ this_result.season = season
+
+ if game_type is not None:
+ this_result.game_type = game_type
+
+ if short_game is not None:
+ if not short_game:
+ this_result.short_game = None
+ else:
+ this_result.short_game = short_game
+
+ if this_result.save() == 1:
+ return_val = model_to_dict(this_result)
+ db.close()
+ return return_val
+ else:
+ db.close()
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that event'
+ )
+
+
+@router.delete('/{result_id}')
+async def delete_result(result_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post results. This event has been logged.'
+ )
+ try:
+ this_result = Result.get_by_id(result_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No result found with id {result_id}')
+
+ count = this_result.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Result {result_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Result {result_id} was not deleted')
+
diff --git a/app/routers_v2/rewards.py b/app/routers_v2/rewards.py
new file mode 100644
index 0000000..26c8670
--- /dev/null
+++ b/app/routers_v2/rewards.py
@@ -0,0 +1,187 @@
+from datetime import datetime
+from fastapi import APIRouter, Depends, HTTPException, Response
+from typing import Optional
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Reward, model_to_dict, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/rewards',
+ tags=['rewards']
+)
+
+
+class RewardModel(pydantic.BaseModel):
+ name: str
+ season: int
+ week: int
+ team_id: int
+ created: Optional[int] = int(datetime.timestamp(datetime.now())*1000)
+
+
+@router.get('')
+async def get_rewards(
+ name: Optional[str] = None, in_name: Optional[str] = None, team_id: Optional[int] = None,
+ season: Optional[int] = None, week: Optional[int] = None, created_after: Optional[int] = None,
+ flat: Optional[bool] = False, csv: Optional[bool] = None):
+ all_rewards = Reward.select()
+
+ if all_rewards.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'There are no rewards to filter')
+
+ if name is not None:
+ all_rewards = all_rewards.where(fn.Lower(Reward.name) == name.lower())
+ if team_id is not None:
+ all_rewards = all_rewards.where(Reward.team_id == team_id)
+ if created_after is not None:
+ all_rewards = all_rewards.where(Reward.created >= created_after)
+ if in_name is not None:
+ all_rewards = all_rewards.where(fn.Lower(Reward.name).contains(in_name.lower()))
+ if season is not None:
+ all_rewards = all_rewards.where(Reward.season == season)
+ if week is not None:
+ all_rewards = all_rewards.where(Reward.week == week)
+
+ if all_rewards.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No rewards found')
+
+ if csv:
+ data_list = [['id', 'name', 'team', 'daily', 'created']]
+ for line in all_rewards:
+ data_list.append(
+ [
+ line.id, line.name, line.team.id, line.daily, line.created
+ ]
+ )
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = {'count': all_rewards.count(), 'rewards': []}
+ for x in all_rewards:
+ return_val['rewards'].append(model_to_dict(x, recurse=not flat))
+
+ db.close()
+ return return_val
+
+
+@router.get('/{reward_id}')
+async def get_one_reward(reward_id, csv: Optional[bool] = False):
+ try:
+ this_reward = Reward.get_by_id(reward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
+
+ if csv:
+ data_list = [
+ ['id', 'name', 'card_count', 'description'],
+ [this_reward.id, this_reward.name, this_reward.team.id, this_reward.daily, this_reward.created]
+ ]
+ return_val = DataFrame(data_list).to_csv(header=False, index=False)
+
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_val = model_to_dict(this_reward)
+ db.close()
+ return return_val
+
+
+@router.post('')
+async def post_rewards(reward: RewardModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post rewards. This event has been logged.'
+ )
+
+ this_reward = Reward(**reward.dict())
+
+ saved = this_reward.save()
+ if saved == 1:
+ return_val = model_to_dict(this_reward)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that cardset'
+ )
+
+
+@router.patch('/{reward_id}')
+async def patch_reward(
+ reward_id, name: Optional[str] = None, team_id: Optional[int] = None, created: Optional[int] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to patch rewards. This event has been logged.'
+ )
+ try:
+ this_reward = Reward.get_by_id(reward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
+
+ if name is not None:
+ this_reward.name = name
+ if team_id is not None:
+ this_reward.team_id = team_id
+ if created is not None:
+ this_reward.created = created
+
+ if this_reward.save() == 1:
+ return_val = model_to_dict(this_reward)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that rarity'
+ )
+
+
+@router.delete('/{reward_id}')
+async def delete_reward(reward_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete rewards. This event has been logged.'
+ )
+ try:
+ this_reward = Reward.get_by_id(reward_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No reward found with id {reward_id}')
+
+ count = this_reward.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Reward {reward_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Reward {reward_id} was not deleted')
+
+
diff --git a/app/routers_v2/scouting.py b/app/routers_v2/scouting.py
new file mode 100644
index 0000000..b412ae6
--- /dev/null
+++ b/app/routers_v2/scouting.py
@@ -0,0 +1,102 @@
+import csv
+from datetime import datetime
+from fastapi import APIRouter, Depends, HTTPException, Response, Query
+from typing import Optional
+import logging
+import pydantic
+import pandas as pd
+
+from ..db_engine import db, model_to_dict, fn, query_to_csv, complex_data_to_csv, Player, BattingCardRatings
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp
+from ..player_scouting import get_player_ids
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/scouting',
+ tags=['scouting']
+)
+
+
+class BattingFiles(pydantic.BaseModel):
+ vl_basic: str = 'vl-basic.csv'
+ vl_rate: str = 'vl-rate.csv'
+ vr_basic: str = 'vr-basic.csv'
+ vr_rate: str = 'vr-rate.csv'
+ running: str = 'running.csv'
+
+
+# def csv_file_to_dataframe(filename: str) -> pd.DataFrame | None:
+# with open(filename, 'r', encoding='utf8') as file:
+# reader = csv.reader(file)
+#
+# for row in reader:
+
+
+@router.get('/playerkeys')
+async def get_player_keys(player_id: list = Query(default=None)):
+ all_keys = []
+ for x in player_id:
+ this_player = Player.get_or_none(Player.player_id == x)
+ if this_player is not None:
+ this_keys = get_player_ids(this_player.bbref_id, id_type='bbref')
+ if this_keys is not None:
+ all_keys.append(this_keys)
+
+ return_val = {'count': len(all_keys), 'keys': [
+ dict(x) for x in all_keys
+ ]}
+ db.close()
+ return return_val
+
+
+@router.post('/live-update/batting')
+def live_update_batting(files: BattingFiles, cardset_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to initiate live updates.'
+ )
+
+ data = {} # : { 'vL': [combined vl stat data], 'vR': [combined vr stat data] }
+ for row in files.vl_basic:
+ if row['pa'] >= 20:
+ data[row['fgid']]['vL'] = row
+ for row in files.vl_rate:
+ if row['fgid'] in data.keys():
+ data[row['fgid']]['vL'].extend(row)
+
+ for row in files.vr_basic:
+ if row['pa'] >= 40 and row['fgid'] in data.keys():
+ data[row['fgid']]['vR'] = row
+ for row in files.vr_rate:
+ if row['fgid'] in data.keys():
+ data[row['fgid']]['vR'].extend(row)
+
+ for x in data.items():
+ pass
+ # Create BattingCardRating object for vL
+ # Create BattingCardRating object for vR
+
+ # Read running stats and create/update BattingCard object
+
+ return files.dict()
+
+
+@router.post('/live-update/pitching')
+def live_update_pitching(files: BattingFiles, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to initiate live updates.'
+ )
+
+ return files.dict()
diff --git a/app/routers_v2/stratgame.py b/app/routers_v2/stratgame.py
new file mode 100644
index 0000000..847f671
--- /dev/null
+++ b/app/routers_v2/stratgame.py
@@ -0,0 +1,178 @@
+from fastapi import APIRouter, Depends, HTTPException, Query, Response
+from typing import Literal, Optional, List
+import logging
+import pandas as pd
+import pydantic
+from pydantic import validator, root_validator
+
+from ..db_engine import db, StratGame, model_to_dict, chunked, PitchingCard, Player, query_to_csv, Team, fn
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/games',
+ tags=['games']
+)
+
+
+class GameModel(pydantic.BaseModel):
+ season: int
+ game_type: str
+ away_team_id: int
+ home_team_id: int
+ week: int = 1
+ away_score: int = 0
+ home_score: int = 0
+ away_team_value: int = None
+ home_team_value: int = None
+ away_team_ranking: int = None
+ home_team_ranking: int = None
+ ranked: bool = False
+ short_game: bool = False
+ forfeit: bool = False
+
+
+class GameList(pydantic.BaseModel):
+ games: List[GameModel]
+
+
+@router.get('')
+async def get_games(
+ season: list = Query(default=None), forfeit: Optional[bool] = None, away_team_id: list = Query(default=None),
+ home_team_id: list = Query(default=None), team1_id: list = Query(default=None),
+ team2_id: list = Query(default=None), game_type: list = Query(default=None), ranked: Optional[bool] = None,
+ short_game: Optional[bool] = None, csv: Optional[bool] = False, short_output: bool = False):
+ all_games = StratGame.select()
+
+ if season is not None:
+ all_games = all_games.where(StratGame.season << season)
+ if forfeit is not None:
+ all_games = all_games.where(StratGame.forfeit == forfeit)
+ if away_team_id is not None:
+ all_games = all_games.where(StratGame.away_team_id << away_team_id)
+ if home_team_id is not None:
+ all_games = all_games.where(StratGame.home_team_id << home_team_id)
+ if team1_id is not None:
+ all_games = all_games.where(
+ (StratGame.away_team_id << team1_id) | (StratGame.home_team_id << team1_id)
+ )
+ if team2_id is not None:
+ all_games = all_games.where(
+ (StratGame.away_team_id << team2_id) | (StratGame.home_team_id << team2_id)
+ )
+ if game_type is not None:
+ g_list = [x.lower() for x in game_type]
+ all_games = all_games.where(fn.Lower(StratGame.game_type) << g_list)
+ if ranked is not None:
+ all_games = all_games.where(StratGame.ranked == ranked)
+ if short_game is not None:
+ all_games = all_games.where(StratGame.short_game == short_game)
+
+ if csv:
+ return_vals = [model_to_dict(x) for x in all_games]
+ for x in return_vals:
+ x['away_abbrev'] = x['away_team']['abbrev']
+ x['home_abbrev'] = x['home_team']['abbrev']
+ del x['away_team'], x['home_team']
+
+ db.close()
+ output = pd.DataFrame(return_vals)[[
+ 'id', 'away_abbrev', 'home_abbrev', 'away_score', 'home_score', 'away_team_value', 'home_team_value',
+ 'game_type', 'season', 'week', 'short_game', 'ranked'
+ ]]
+
+ return Response(content=output.to_csv(index=False), media_type='text/csv')
+
+ return_val = {'count': all_games.count(), 'games': [
+ model_to_dict(x, recurse=not short_output) for x in all_games
+ ]}
+ db.close()
+ return return_val
+
+
+@router.get('/{game_id}')
+async def get_one_game(game_id: int):
+ this_game = StratGame.get_or_none(StratGame.id == game_id)
+ if not this_game:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found')
+
+ g_result = model_to_dict(this_game)
+ db.close()
+ return g_result
+
+
+@router.patch('/{game_id}')
+async def patch_game(
+ game_id: int, game_type: Optional[str] = None, away_score: Optional[int] = None,
+ home_score: Optional[int] = None, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'patch_game - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_game = StratGame.get_or_none(StratGame.id == game_id)
+ if not this_game:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found')
+
+ if away_score is not None:
+ this_game.away_score = away_score
+ if home_score is not None:
+ this_game.home_score = home_score
+ if game_type is not None:
+ this_game.game_type = game_type
+
+ if this_game.save() == 1:
+ g_result = model_to_dict(this_game)
+ db.close()
+ return g_result
+ else:
+ db.close()
+ raise HTTPException(status_code=500, detail=f'Unable to patch game {game_id}')
+
+
+@router.post('')
+async def post_game(this_game: GameModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'post_games - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_game = StratGame(**this_game.dict())
+
+ saved = this_game.save()
+ if saved == 1:
+ return_val = model_to_dict(this_game)
+ db.close()
+ return return_val
+ else:
+ raise HTTPException(
+ status_code=418,
+ detail='Well slap my ass and call me a teapot; I could not save that game'
+ )
+
+
+@router.delete('/{game_id}')
+async def delete_game(game_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'delete_game - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_game = StratGame.get_or_none(StratGame.id == game_id)
+ if not this_game:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'StratGame ID {game_id} not found')
+
+ count = this_game.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'StratGame {game_id} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'StratGame {game_id} could not be deleted')
+
+
diff --git a/app/routers_v2/stratplays.py b/app/routers_v2/stratplays.py
new file mode 100644
index 0000000..359cf98
--- /dev/null
+++ b/app/routers_v2/stratplays.py
@@ -0,0 +1,1054 @@
+import math
+
+from fastapi import APIRouter, Depends, HTTPException, Query, Response
+from typing import List, Optional, Literal
+import logging
+import pandas as pd
+from pydantic import BaseModel, validator
+
+from ..db_engine import db, StratPlay, StratGame, Team, Player, model_to_dict, chunked, fn, SQL, \
+ complex_data_to_csv, Decision
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/plays',
+ tags=['plays']
+)
+
+POS_LIST = Literal['C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'P', 'DH', 'PH', 'PR', 'GHOST']
+
+
+class PlayModel(BaseModel):
+ game_id: int
+ play_num: int
+ batter_id: int = None
+ batter_team_id: int = None
+ pitcher_id: int
+ pitcher_team_id: int = None
+ on_base_code: str
+ inning_half: Literal['top', 'bot', 'Top', 'Bot']
+ inning_num: int
+ batting_order: int
+ starting_outs: int
+ away_score: int
+ home_score: int
+ batter_pos: POS_LIST = None
+
+ on_first_id: int = None
+ on_first_final: int = None
+ on_second_id: int = None
+ on_second_final: int = None
+ on_third_id: int = None
+ on_third_final: int = None
+ batter_final: int = None
+
+ pa: int = 0
+ ab: int = 0
+ e_run: int = 0
+ run: int = 0
+ hit: int = 0
+ rbi: int = 0
+ double: int = 0
+ triple: int = 0
+ homerun: int = 0
+ bb: int = 0
+ so: int = 0
+ hbp: int = 0
+ sac: int = 0
+ ibb: int = 0
+ gidp: int = 0
+ bphr: int = 0
+ bpfo: int = 0
+ bp1b: int = 0
+ bplo: int = 0
+ sb: int = 0
+ cs: int = 0
+ outs: int = 0
+ wpa: float = 0.0
+ re24: float = 0.0
+
+ catcher_id: int = None
+ catcher_team_id: int = None
+ defender_id: int = None
+ defender_team_id: int = None
+ runner_id: int = None
+ runner_team_id: int = None
+
+ check_pos: POS_LIST = None
+ error: int = 0
+ wild_pitch: int = 0
+ passed_ball: int = 0
+ pick_off: int = 0
+ balk: int = 0
+ is_go_ahead: bool = False
+ is_tied: bool = False
+ is_new_inning: bool = False
+
+ @validator('on_first_final')
+ def no_final_if_no_runner_one(cls, v, values):
+ if values['on_first_id'] is None:
+ return None
+ return v
+
+ @validator('on_second_final')
+ def no_final_if_no_runner_two(cls, v, values):
+ if values['on_second_id'] is None:
+ return None
+ return v
+
+ @validator('on_third_final')
+ def no_final_if_no_runner_three(cls, v, values):
+ if values['on_third_id'] is None:
+ return None
+ return v
+
+ @validator('batter_final')
+ def no_final_if_no_batter(cls, v, values):
+ if values['batter_id'] is None:
+ return None
+ return v
+
+
+class PlayList(BaseModel):
+ plays: List[PlayModel]
+
+
+@router.get('')
+async def get_plays(
+ game_id: list = Query(default=None), batter_id: list = Query(default=None), season: list = Query(default=None),
+ week: list = Query(default=None), has_defender: Optional[bool] = None, has_catcher: Optional[bool] = None,
+ has_defender_or_catcher: Optional[bool] = None, is_scoring_play: Optional[bool] = None,
+ pitcher_id: list = Query(default=None), obc: list = Query(default=None), inning: list = Query(default=None),
+ batting_order: list = Query(default=None), starting_outs: list = Query(default=None),
+ batter_pos: list = Query(default=None), catcher_id: list = Query(default=None),
+ defender_id: list = Query(default=None), runner_id: list = Query(default=None),
+ offense_team_id: list = Query(default=None), defense_team_id: list = Query(default=None),
+ hit: Optional[int] = None, double: Optional[int] = None, triple: Optional[int] = None,
+ homerun: Optional[int] = None, play_num: list = Query(default=None), game_type: list = Query(default=None),
+ sb: Optional[int] = None, cs: Optional[int] = None, csv: Optional[bool] = False,
+ run: Optional[int] = None, e_run: Optional[int] = None, rbi: list = Query(default=None),
+ outs: list = Query(default=None), wild_pitch: Optional[int] = None, is_final_out: Optional[bool] = None,
+ is_go_ahead: Optional[bool] = None, is_tied: Optional[bool] = None, is_new_inning: Optional[bool] = None,
+ min_wpa: Optional[float] = None, max_wpa: Optional[float] = None, sort: Optional[str] = None,
+ short_output: Optional[bool] = False, limit: Optional[int] = 200, page_num: Optional[int] = 1):
+ all_plays = StratPlay.select()
+
+ if season is not None:
+ s_games = StratGame.select().where(StratGame.season << season)
+ all_plays = all_plays.where(StratPlay.game << s_games)
+ if week is not None:
+ w_games = StratGame.select().where(StratGame.week << week)
+ all_plays = all_plays.where(StratPlay.game << w_games)
+ if has_defender is not None:
+ all_plays = all_plays.where(StratPlay.defender.is_null(False))
+ if has_catcher is not None:
+ all_plays = all_plays.where(StratPlay.catcher.is_null(False))
+ if has_defender_or_catcher is not None:
+ all_plays = all_plays.where(
+ (StratPlay.catcher.is_null(False)) | (StratPlay.defender.is_null(False))
+ )
+ if game_id is not None:
+ all_plays = all_plays.where(StratPlay.game_id << game_id)
+ if batter_id is not None:
+ all_plays = all_plays.where(StratPlay.batter_id << batter_id)
+ if pitcher_id is not None:
+ all_plays = all_plays.where(StratPlay.pitcher_id << pitcher_id)
+ if obc is not None:
+ all_plays = all_plays.where(StratPlay.on_base_code << obc)
+ if inning is not None:
+ all_plays = all_plays.where(StratPlay.inning_num << inning)
+ if batting_order is not None:
+ all_plays = all_plays.where(StratPlay.batting_order << batting_order)
+ if starting_outs is not None:
+ all_plays = all_plays.where(StratPlay.starting_outs << starting_outs)
+ if batter_pos is not None:
+ all_plays = all_plays.where(StratPlay.batter_pos << batter_pos)
+ if catcher_id is not None:
+ all_plays = all_plays.where(StratPlay.catcher_id << catcher_id)
+ if defender_id is not None:
+ all_plays = all_plays.where(StratPlay.defender_id << defender_id)
+ if runner_id is not None:
+ all_plays = all_plays.where(StratPlay.runner_id << runner_id)
+ if offense_team_id is not None:
+ all_teams = Team.select().where(Team.id << offense_team_id)
+ all_plays = all_plays.where(
+ (StratPlay.batter_team << all_teams) | (StratPlay.runner_team << all_teams)
+ )
+ if defense_team_id is not None:
+ all_teams = Team.select().where(Team.id << defense_team_id)
+ all_plays = all_plays.where(
+ (StratPlay.catcher_team << all_teams) | (StratPlay.defender_team << all_teams)
+ )
+ if hit is not None:
+ all_plays = all_plays.where(StratPlay.hit == hit)
+ if double is not None:
+ all_plays = all_plays.where(StratPlay.double == double)
+ if triple is not None:
+ all_plays = all_plays.where(StratPlay.triple == triple)
+ if homerun is not None:
+ all_plays = all_plays.where(StratPlay.homerun == homerun)
+ if sb is not None:
+ all_plays = all_plays.where(StratPlay.sb == sb)
+ if cs is not None:
+ all_plays = all_plays.where(StratPlay.cs == cs)
+ if wild_pitch is not None:
+ all_plays = all_plays.where(StratPlay.wild_pitch == wild_pitch)
+ if run is not None:
+ all_plays = all_plays.where(StratPlay.run == run)
+ if e_run is not None:
+ all_plays = all_plays.where(StratPlay.e_run == e_run)
+ if rbi is not None:
+ all_plays = all_plays.where(StratPlay.rbi << rbi)
+ if outs is not None:
+ all_plays = all_plays.where(StratPlay.outs << outs)
+ if is_final_out is not None:
+ all_plays = all_plays.where(StratPlay.starting_outs + StratPlay.outs == 3)
+ if is_go_ahead is not None:
+ all_plays = all_plays.where(StratPlay.is_go_ahead == is_go_ahead)
+ if is_tied is not None:
+ all_plays = all_plays.where(StratPlay.is_tied == is_tied)
+ if is_new_inning is not None:
+ all_plays = all_plays.where(StratPlay.is_new_inning == is_new_inning)
+ if is_scoring_play is not None:
+ all_plays = all_plays.where(
+ (StratPlay.on_first_final == 4) | (StratPlay.on_second_final == 4) | (StratPlay.on_third_final == 4) |
+ (StratPlay.batter_final == 4)
+ )
+ if min_wpa is not None:
+ all_plays = all_plays.where(StratPlay.wpa >= min_wpa)
+ if max_wpa is not None:
+ all_plays = all_plays.where(StratPlay.wpa <= max_wpa)
+ if play_num is not None:
+ all_plays = all_plays.where(StratPlay.play_num << play_num)
+ if game_type is not None:
+ all_types = [x.lower() for x in game_type]
+ all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types)
+ all_plays = all_plays.where(StratPlay.game << all_games)
+
+ if limit > 5000:
+ limit = 5000
+ elif limit < 1:
+ limit = 1
+ if page_num < 1:
+ page_num = 1
+
+ if sort == 'wpa-desc':
+ all_plays = all_plays.order_by(-fn.ABS(StratPlay.wpa))
+ elif sort == 'wpa-asc':
+ all_plays = all_plays.order_by(fn.ABS(StratPlay.wpa))
+ elif sort == 're24-desc':
+ all_plays = all_plays.order_by(-fn.ABS(StratPlay.re24))
+ elif sort == 're24-asc':
+ all_plays = all_plays.order_by(fn.ABS(StratPlay.re24))
+ elif sort == 'newest':
+ all_plays = all_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc())
+ elif sort == 'oldest':
+ all_plays = all_plays.order_by(StratPlay.game_id, StratPlay.play_num)
+
+ all_plays = all_plays.paginate(page_num, limit)
+
+ if csv:
+ return_vals = [model_to_dict(x) for x in all_plays]
+ for x in return_vals:
+ x['game_id'] = x['game']['id']
+ x['game_type'] = x['game']['game_type']
+ x['batter_id'] = x['batter']['player_id']
+ x['batter_name'] = x['batter']['p_name']
+ x['batter_cardset'] = x['batter']['cardset']['name']
+ x['batter_team_id'] = x['batter_team']['id']
+ x['batter_team_abbrev'] = x['batter_team']['abbrev']
+ x['pitcher_id'] = x['pitcher']['player_id']
+ x['pitcher_name'] = x['pitcher']['p_name']
+ x['pitcher_cardset'] = x['pitcher']['cardset']['name']
+ x['pitcher_team_id'] = x['pitcher_team']['id']
+ x['pitcher_team_abbrev'] = x['pitcher_team']['abbrev']
+
+ if x['catcher'] is not None:
+ x['catcher_id'] = x['catcher']['player_id']
+ x['catcher_name'] = x['catcher']['p_name']
+ x['catcher_cardset'] = x['catcher']['cardset']['name']
+ x['catcher_team_id'] = x['catcher_team']['id']
+ x['catcher_team_abbrev'] = x['catcher_team']['abbrev']
+ else:
+ x['catcher_id'] = None
+ x['catcher_name'] = None
+ x['catcher_cardset'] = None
+ x['catcher_team_id'] = None
+ x['catcher_team_abbrev'] = None
+
+ if x['defender'] is not None:
+ x['defender_id'] = x['defender']['player_id']
+ x['defender_name'] = x['defender']['p_name']
+ x['defender_cardset'] = x['defender']['cardset']['name']
+ x['defender_team_id'] = x['defender_team']['id']
+ x['defender_team_abbrev'] = x['defender_team']['abbrev']
+ else:
+ x['defender_id'] = None
+ x['defender_name'] = None
+ x['defender_cardset'] = None
+ x['defender_team_id'] = None
+ x['defender_team_abbrev'] = None
+
+ if x['runner'] is not None:
+ x['runner_id'] = x['runner']['player_id']
+ x['runner_name'] = x['runner']['p_name']
+ x['runner_cardset'] = x['runner']['cardset']['name']
+ x['runner_team_id'] = x['runner_team']['id']
+ x['runner_team_abbrev'] = x['runner_team']['abbrev']
+ else:
+ x['runner_id'] = None
+ x['runner_name'] = None
+ x['runner_cardset'] = None
+ x['runner_team_id'] = None
+ x['runner_team_abbrev'] = None
+
+ del x['game'], x['batter'], x['batter_team'], x['pitcher'], x['pitcher_team'], x['catcher'], \
+ x['catcher_team'], x['defender'], x['defender_team'], x['runner'], x['runner_team']
+
+ db.close()
+ return Response(content=pd.DataFrame(return_vals).to_csv(index=False), media_type='text/csv')
+
+ return_plays = {
+ 'count': all_plays.count(),
+ 'plays': [model_to_dict(x, recurse=not short_output) for x in all_plays]
+ }
+ db.close()
+ return return_plays
+
+
+@router.get('/batting')
+async def get_batting_totals(
+ season: list = Query(default=None), week: list = Query(default=None), position: list = Query(default=None),
+ player_id: list = Query(default=None), min_wpa: Optional[float] = -999, max_wpa: Optional[float] = 999,
+ group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league', 'gametype'] = 'player',
+ min_pa: Optional[int] = 1, team_id: list = Query(default=None), inning: list = Query(default=None),
+ obc: list = Query(default=None), risp: Optional[bool] = None, game_type: list = Query(default=None),
+ page_num: Optional[int] = 1, sort: Optional[str] = 'pa-desc', limit: Optional[int] = 500,
+ short_output: Optional[bool] = False, csv: Optional[bool] = False):
+ season_games = StratGame.select()
+ if season is not None:
+ season_games = season_games.where(StratGame.season << season)
+ if week is not None:
+ season_games = season_games.where(StratGame.week << week)
+
+ bat_plays = (
+ StratPlay
+ .select(StratPlay.batter, StratPlay.game, fn.SUM(StratPlay.pa).alias('sum_pa'),
+ fn.SUM(StratPlay.ab).alias('sum_ab'), fn.SUM(StratPlay.run).alias('sum_run'),
+ fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'),
+ fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'),
+ fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bb).alias('sum_bb'),
+ fn.SUM(StratPlay.so).alias('sum_so'), StratPlay.batter_team,
+ fn.SUM(StratPlay.hbp).alias('sum_hbp'), fn.SUM(StratPlay.sac).alias('sum_sac'),
+ fn.SUM(StratPlay.ibb).alias('sum_ibb'), fn.SUM(StratPlay.gidp).alias('sum_gidp'),
+ fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'),
+ fn.SUM(StratPlay.bphr).alias('sum_bphr'), fn.SUM(StratPlay.bpfo).alias('sum_bpfo'),
+ fn.SUM(StratPlay.bp1b).alias('sum_bp1b'), fn.SUM(StratPlay.bplo).alias('sum_bplo'),
+ fn.SUM(StratPlay.wpa).alias('sum_wpa'), fn.SUM(StratPlay.re24).alias('sum_re24'),
+ fn.COUNT(StratPlay.on_first_final).filter(
+ StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4)).alias('count_lo1'),
+ fn.COUNT(StratPlay.on_second_final).filter(
+ StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4)).alias('count_lo2'),
+ fn.COUNT(StratPlay.on_third_final).filter(
+ StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4)).alias('count_lo3'),
+ fn.COUNT(StratPlay.on_first).filter(StratPlay.on_first.is_null(False)).alias('count_runner1'),
+ fn.COUNT(StratPlay.on_second).filter(StratPlay.on_second.is_null(False)).alias('count_runner2'),
+ fn.COUNT(StratPlay.on_third).filter(StratPlay.on_third.is_null(False)).alias('count_runner3'),
+ fn.COUNT(StratPlay.on_first_final).filter(
+ StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4) &
+ (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo1_3out'),
+ fn.COUNT(StratPlay.on_second_final).filter(
+ StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4) &
+ (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo2_3out'),
+ fn.COUNT(StratPlay.on_third_final).filter(
+ StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4) &
+ (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo3_3out')
+ # fn.COUNT(StratPlay.on_first).filter(StratPlay.on_first.is_null(False) &
+ # (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_runner1_3out'),
+ # fn.COUNT(StratPlay.on_second).filter(StratPlay.on_second.is_null(False) &
+ # (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_runner2_3out'),
+ # fn.COUNT(StratPlay.on_third).filter(StratPlay.on_third.is_null(False) &
+ # (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_runner3_3out')
+ )
+ .where((StratPlay.game << season_games) & (StratPlay.batter.is_null(False)))
+ .having(fn.SUM(StratPlay.pa) >= min_pa)
+ )
+ run_plays = (
+ StratPlay
+ .select(StratPlay.runner, StratPlay.runner_team, fn.SUM(StratPlay.sb).alias('sum_sb'),
+ fn.SUM(StratPlay.cs).alias('sum_cs'), fn.SUM(StratPlay.pick_off).alias('sum_pick'),
+ fn.SUM(StratPlay.wpa).alias('sum_wpa'), fn.SUM(StratPlay.re24).alias('sum_re24'))
+ .where((StratPlay.game << season_games) & (StratPlay.runner.is_null(False)))
+ )
+
+ if player_id is not None:
+ all_players = Player.select().where(Player.player_id << player_id)
+ bat_plays = bat_plays.where(StratPlay.batter << all_players)
+ run_plays = run_plays.where(StratPlay.runner << all_players)
+ if team_id is not None:
+ all_teams = Team.select().where(Team.id << team_id)
+ bat_plays = bat_plays.where(StratPlay.batter_team << all_teams)
+ run_plays = run_plays.where(StratPlay.runner_team << all_teams)
+ if position is not None:
+ bat_plays = bat_plays.where(StratPlay.batter_pos << position)
+
+ if obc is not None:
+ bat_plays = bat_plays.where(StratPlay.on_base_code << obc)
+ if risp is not None:
+ bat_plays = bat_plays.where(StratPlay.on_base_code << ['100', '101', '110', '111', '010', '011'])
+ if inning is not None:
+ bat_plays = bat_plays.where(StratPlay.inning_num << inning)
+ if game_type is not None:
+ all_types = [x.lower() for x in game_type]
+ all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types)
+ bat_plays = bat_plays.where(StratPlay.game << all_games)
+ run_plays = run_plays.where(StratPlay.game << all_games)
+
+ if group_by is not None:
+ if group_by == 'player':
+ bat_plays = bat_plays.group_by(StratPlay.batter)
+ run_plays = run_plays.group_by(StratPlay.runner)
+ elif group_by == 'team':
+ bat_plays = bat_plays.group_by(StratPlay.batter_team)
+ run_plays = run_plays.group_by(StratPlay.runner_team)
+ elif group_by == 'playerteam':
+ bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.batter_team)
+ run_plays = run_plays.group_by(StratPlay.runner, StratPlay.runner_team)
+ elif group_by == 'playergame':
+ bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.game)
+ run_plays = run_plays.group_by(StratPlay.runner, StratPlay.game)
+ elif group_by == 'teamgame':
+ bat_plays = bat_plays.group_by(StratPlay.batter_team, StratPlay.game)
+ run_plays = run_plays.group_by(StratPlay.runner_team, StratPlay.game)
+ elif group_by == 'league':
+ bat_plays = bat_plays.join(StratGame)
+ bat_plays = bat_plays.group_by(StratPlay.game.season)
+ run_plays = run_plays.join(StratGame)
+ run_plays = run_plays.group_by(StratPlay.game.season)
+ elif group_by == 'gametype':
+ bat_plays = bat_plays.join(StratGame)
+ bat_plays = bat_plays.group_by(StratPlay.game.game_type)
+ run_plays = run_plays.join(StratGame)
+ run_plays = run_plays.group_by(StratPlay.game.game_type)
+ elif group_by == 'playerteamgametype':
+ bat_plays = bat_plays.join(StratGame)
+ bat_plays = bat_plays.group_by(StratPlay.batter, StratPlay.batter_team, StratPlay.game.game_type)
+ run_plays = run_plays.join(StratGame)
+ run_plays = run_plays.group_by(StratPlay.runner, StratPlay.runner_team, StratPlay.game.game_type)
+
+ if sort is not None:
+ if sort == 'player':
+ bat_plays = bat_plays.order_by(StratPlay.batter)
+ run_plays = run_plays.order_by(StratPlay.runner)
+ elif sort == 'team':
+ bat_plays = bat_plays.order_by(StratPlay.batter_team)
+ run_plays = run_plays.order_by(StratPlay.runner_team)
+ elif sort == 'wpa-desc':
+ bat_plays = bat_plays.order_by(SQL('sum_wpa').desc())
+ elif sort == 'wpa-asc':
+ bat_plays = bat_plays.order_by(SQL('sum_wpa').asc())
+ elif sort == 'pa-desc':
+ bat_plays = bat_plays.order_by(SQL('sum_pa').desc())
+ elif sort == 'pa-asc':
+ bat_plays = bat_plays.order_by(SQL('sum_pa').asc())
+ elif sort == 're24-desc':
+ bat_plays = bat_plays.order_by(SQL('sum_re24').desc())
+ elif sort == 're24-asc':
+ bat_plays = bat_plays.order_by(SQL('sum_re24').asc())
+ elif sort == 'newest':
+ bat_plays = bat_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc())
+ run_plays = run_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc())
+ elif sort == 'oldest':
+ bat_plays = bat_plays.order_by(StratPlay.game_id, StratPlay.play_num)
+ run_plays = run_plays.order_by(StratPlay.game_id, StratPlay.play_num)
+
+ if limit < 1:
+ limit = 1
+ elif limit > 500:
+ limit = 500
+ bat_plays = bat_plays.paginate(page_num, limit)
+
+ logging.info(f'bat_plays query: {bat_plays}')
+ logging.info(f'run_plays query: {run_plays}')
+
+ return_stats = {
+ 'count': bat_plays.count(),
+ 'stats': []
+ }
+
+ for x in bat_plays:
+ this_run = run_plays.order_by(StratPlay.id)
+ if 'player' in group_by:
+ this_run = this_run.where(StratPlay.runner == x.batter)
+ if 'game' in group_by:
+ this_run = this_run.where(StratPlay.game == x.game)
+
+ if this_run.count() > 0:
+ sum_sb = this_run[0].sum_sb
+ sum_cs = this_run[0].sum_cs
+ run_wpa = this_run[0].sum_wpa
+ run_re24 = this_run[0].sum_re24
+ else:
+ sum_sb = 0
+ sum_cs = 0
+ run_wpa = 0
+ run_re24 = 0
+ this_wpa = bat_plays.where(
+ (StratPlay.wpa >= min_wpa) & (StratPlay.wpa <= max_wpa) & (StratPlay.batter == x.batter)
+ )
+ if this_wpa.count() > 0:
+ sum_wpa = this_wpa[0].sum_wpa
+ else:
+ sum_wpa = 0
+
+ tot_ab = x.sum_ab if x.sum_ab > 0 else 1
+ obp = (x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / x.sum_pa
+ slg = (x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 +
+ (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / tot_ab
+
+ this_game = 'TOT'
+ if group_by in ['playergame', 'teamgame']:
+ this_game = x.game_id if short_output else model_to_dict(x.game, recurse=False)
+
+ lob_all_rate, lob_2outs_rate, rbi_rate = 0, 0, 0
+ if x.count_runner1 + x.count_runner2 + x.count_runner3 > 0:
+ lob_all_rate = (x.count_lo1 + x.count_lo2 + x.count_lo3) / \
+ (x.count_runner1 + x.count_runner2 + x.count_runner3)
+ rbi_rate = (x.sum_rbi - x.sum_hr) / (x.count_runner1 + x.count_runner2 + x.count_runner3)
+
+ return_stats['stats'].append({
+ 'player': x.batter_id if short_output else model_to_dict(x.batter, recurse=True, max_depth=1),
+ 'team': x.batter_team_id if short_output else model_to_dict(x.batter_team, recurse=True, max_depth=1),
+ 'pa': x.sum_pa,
+ 'ab': x.sum_ab,
+ 'run': x.sum_run,
+ 'hit': x.sum_hit,
+ 'rbi': x.sum_rbi,
+ 'double': x.sum_double,
+ 'triple': x.sum_triple,
+ 'hr': x.sum_hr,
+ 'bb': x.sum_bb,
+ 'so': x.sum_so,
+ 'hbp': x.sum_hbp,
+ 'sac': x.sum_sac,
+ 'ibb': x.sum_ibb,
+ 'gidp': x.sum_gidp,
+ 'sb': sum_sb,
+ 'cs': sum_cs,
+ 'bphr': x.sum_bphr,
+ 'bpfo': x.sum_bpfo,
+ 'bp1b': x.sum_bp1b,
+ 'bplo': x.sum_bplo,
+ 'wpa': sum_wpa + run_wpa,
+ 're24': x.sum_re24 + run_re24,
+ 'avg': x.sum_hit / tot_ab,
+ 'obp': obp,
+ 'slg': slg,
+ 'ops': obp + slg,
+ 'woba': (.69 * x.sum_bb + .72 * x.sum_hbp + .89 * (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr) +
+ 1.27 * x.sum_double + 1.62 * x.sum_triple + 2.1 * x.sum_hr) / max(x.sum_pa - x.sum_ibb, 1),
+ 'game': this_game,
+ 'lob_all': x.count_lo1 + x.count_lo2 + x.count_lo3,
+ 'lob_all_rate': lob_all_rate,
+ 'lob_2outs': x.count_lo1_3out + x.count_lo2_3out + x.count_lo3_3out,
+ 'rbi%': rbi_rate
+ })
+
+ if csv:
+ return_vals = return_stats['stats']
+ if len(return_vals) == 0:
+ return Response(content=pd.DataFrame().to_csv(index=False), media_type='text/csv')
+
+ for x in return_vals:
+ x['player_id'] = x['player']['player_id']
+ x['player_name'] = x['player']['p_name']
+ x['player_cardset'] = x['player']['cardset']['name']
+ x['team_id'] = x['team']['id']
+ x['team_abbrev'] = x['team']['abbrev']
+ del x['player'], x['team']
+
+ output = pd.DataFrame(return_vals)
+ first = ['player_id', 'player_name', 'player_cardset', 'team_id', 'team_abbrev']
+ exclude = first + ['lob_all', 'lob_all_rate', 'lob_2outs', 'rbi%']
+ output = output[first + [col for col in output.columns if col not in exclude]]
+
+ db.close()
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+ db.close()
+ return return_stats
+
+
+@router.get('/pitching')
+async def get_pitching_totals(
+ season: list = Query(default=None), week: list = Query(default=None),
+ s_type: Literal['regular', 'post', 'total', None] = None, player_id: list = Query(default=None),
+ group_by: Literal['team', 'player', 'playerteam', 'playergame', 'teamgame', 'league', 'gametype'] = 'player',
+ min_pa: Optional[int] = 1, team_id: list = Query(default=None), manager_id: list = Query(default=None),
+ obc: list = Query(default=None), risp: Optional[bool] = None, inning: list = Query(default=None),
+ page_num: Optional[int] = 1, game_type: list = Query(default=None), sort: Optional[str] = 'ip-desc',
+ limit: Optional[int] = 500, short_output: Optional[bool] = False, csv: Optional[bool] = False):
+ season_games = StratGame.select()
+ if season is not None:
+ season_games = season_games.where(StratGame.season << season)
+ if week is not None and s_type is not None:
+ raise HTTPException(status_code=400, detail=f'Week and s_type parameters cannot be used in the same query')
+ if week is not None:
+ season_games = season_games.where(StratGame.week << week)
+ if s_type is not None:
+ if s_type == 'regular':
+ season_games = season_games.where(StratGame.week <= 18)
+ elif s_type == 'post':
+ season_games = season_games.where(StratGame.week > 18)
+ if manager_id is not None:
+ season_games = season_games.where(
+ (StratGame.away_manager_id << manager_id) | (StratGame.home_manager_id << manager_id)
+ )
+
+ pit_plays = (
+ StratPlay
+ .select(StratPlay.pitcher, StratPlay.pitcher_team, StratPlay.game, fn.SUM(StratPlay.pa).alias('sum_pa'),
+ fn.SUM(StratPlay.ab).alias('sum_ab'), fn.SUM(StratPlay.run).alias('sum_run'),
+ fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'),
+ fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'),
+ fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bb).alias('sum_bb'),
+ fn.SUM(StratPlay.so).alias('sum_so'), fn.SUM(StratPlay.wpa).alias('sum_wpa'),
+ fn.SUM(StratPlay.hbp).alias('sum_hbp'), fn.SUM(StratPlay.sac).alias('sum_sac'),
+ fn.SUM(StratPlay.ibb).alias('sum_ibb'), fn.SUM(StratPlay.gidp).alias('sum_gidp'),
+ fn.SUM(StratPlay.sb).alias('sum_sb'), fn.SUM(StratPlay.cs).alias('sum_cs'),
+ fn.SUM(StratPlay.bphr).alias('sum_bphr'), fn.SUM(StratPlay.bpfo).alias('sum_bpfo'),
+ fn.SUM(StratPlay.bp1b).alias('sum_bp1b'), fn.SUM(StratPlay.bplo).alias('sum_bplo'),
+ fn.SUM(StratPlay.wild_pitch).alias('sum_wp'), fn.SUM(StratPlay.balk).alias('sum_balk'),
+ fn.SUM(StratPlay.outs).alias('sum_outs'), fn.SUM(StratPlay.e_run).alias('sum_erun'),
+ fn.SUM(StratPlay.re24).alias('sum_re24'),
+ fn.COUNT(StratPlay.on_first_final).filter(
+ StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4)).alias('count_lo1'),
+ fn.COUNT(StratPlay.on_second_final).filter(
+ StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4)).alias('count_lo2'),
+ fn.COUNT(StratPlay.on_third_final).filter(
+ StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4)).alias('count_lo3'),
+ fn.COUNT(StratPlay.on_first).filter(StratPlay.on_first.is_null(False)).alias('count_runner1'),
+ fn.COUNT(StratPlay.on_second).filter(StratPlay.on_second.is_null(False)).alias('count_runner2'),
+ fn.COUNT(StratPlay.on_third).filter(StratPlay.on_third.is_null(False)).alias('count_runner3'),
+ fn.COUNT(StratPlay.on_first_final).filter(
+ StratPlay.on_first_final.is_null(False) & (StratPlay.on_first_final != 4) &
+ (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo1_3out'),
+ fn.COUNT(StratPlay.on_second_final).filter(
+ StratPlay.on_second_final.is_null(False) & (StratPlay.on_second_final != 4) &
+ (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo2_3out'),
+ fn.COUNT(StratPlay.on_third_final).filter(
+ StratPlay.on_third_final.is_null(False) & (StratPlay.on_third_final != 4) &
+ (StratPlay.starting_outs + StratPlay.outs == 3)).alias('count_lo3_3out'))
+ .where((StratPlay.game << season_games) & (StratPlay.pitcher.is_null(False)))
+ .having(fn.SUM(StratPlay.pa) >= min_pa)
+ )
+ all_dec = (
+ Decision
+ .select(Decision.pitcher, fn.SUM(Decision.win).alias('sum_win'), fn.SUM(Decision.loss).alias('sum_loss'),
+ fn.SUM(Decision.hold).alias('sum_hold'), fn.SUM(Decision.is_save).alias('sum_save'),
+ fn.SUM(Decision.b_save).alias('sum_b_save'), fn.SUM(Decision.irunners).alias('sum_irunners'),
+ fn.SUM(Decision.irunners_scored).alias('sum_irun_scored'),
+ fn.SUM(Decision.is_start).alias('sum_gs'), fn.COUNT(Decision.game).alias('sum_game'))
+ .where(Decision.game << season_games)
+ )
+
+ if player_id is not None:
+ all_players = Player.select().where(Player.player_id << player_id)
+ pit_plays = pit_plays.where(StratPlay.pitcher << all_players)
+ if team_id is not None:
+ all_teams = Team.select().where(Team.id << team_id)
+ pit_plays = pit_plays.where(StratPlay.pitcher_team << all_teams)
+ all_dec = all_dec.where(Decision.pitcher_team << all_teams)
+
+ if obc is not None:
+ pit_plays = pit_plays.where(StratPlay.on_base_code << obc)
+ if risp is not None:
+ pit_plays = pit_plays.where(StratPlay.on_base_code << ['100', '101', '110', '111', '010', '011'])
+ if inning is not None:
+ pit_plays = pit_plays.where(StratPlay.inning_num << inning)
+ if game_type is not None:
+ all_types = [x.lower() for x in game_type]
+ all_games = StratGame.select().where(fn.Lower(StratGame.game_type) << all_types)
+ pit_plays = pit_plays.where(StratPlay.game << all_games)
+
+ if group_by is not None:
+ if group_by == 'player':
+ pit_plays = pit_plays.group_by(StratPlay.pitcher)
+ elif group_by == 'team':
+ pit_plays = pit_plays.group_by(StratPlay.pitcher_team)
+ elif group_by == 'playerteam':
+ pit_plays = pit_plays.group_by(StratPlay.pitcher, StratPlay.pitcher_team)
+ elif group_by == 'playergame':
+ pit_plays = pit_plays.group_by(StratPlay.pitcher, StratPlay.game)
+ elif group_by == 'teamgame':
+ pit_plays = pit_plays.group_by(StratPlay.pitcher_team, StratPlay.game)
+ elif group_by == 'league':
+ pit_plays = pit_plays.join(StratGame)
+ pit_plays = pit_plays.group_by(StratPlay.game.season)
+ elif group_by == 'gametype':
+ pit_plays = pit_plays.join(StratGame)
+ pit_plays = pit_plays.group_by(StratPlay.game.game_type)
+ elif group_by == 'playerteamgametype':
+ pit_plays = pit_plays.join(StratGame)
+ pit_plays = pit_plays.group_by(StratPlay.pitcher, StratPlay.pitcher_team, StratPlay.game.game_type)
+ if sort is not None:
+ if sort == 'player':
+ pit_plays = pit_plays.order_by(StratPlay.pitcher)
+ elif sort == 'team':
+ pit_plays = pit_plays.order_by(StratPlay.pitcher_team)
+ elif sort == 'wpa-desc':
+ pit_plays = pit_plays.order_by(SQL('sum_wpa').asc()) # functions seem reversed since pitcher plays negative
+ elif sort == 'wpa-asc':
+ pit_plays = pit_plays.order_by(SQL('sum_wpa').desc())
+ elif sort == 're24-desc':
+ pit_plays = pit_plays.order_by(SQL('sum_re24').asc()) # functions seem reversed since pitcher plays negative
+ elif sort == 're24-asc':
+ pit_plays = pit_plays.order_by(SQL('sum_re24').desc())
+ elif sort == 'ip-desc':
+ pit_plays = pit_plays.order_by(SQL('sum_outs').desc())
+ elif sort == 'ip-asc':
+ pit_plays = pit_plays.order_by(SQL('sum_outs').asc())
+ elif sort == 'game-desc':
+ pit_plays = pit_plays.order_by(SQL('sum_game').desc())
+ elif sort == 'game-asc':
+ pit_plays = pit_plays.order_by(SQL('sum_game').asc())
+ elif sort == 'newest':
+ pit_plays = pit_plays.order_by(StratPlay.game_id.desc(), StratPlay.play_num.desc())
+ elif sort == 'oldest':
+ pit_plays = pit_plays.order_by(StratPlay.game_id, StratPlay.play_num)
+
+ if limit < 1:
+ limit = 1
+ elif limit > 500:
+ limit = 500
+ pit_plays = pit_plays.paginate(page_num, limit)
+
+ return_stats = {
+ 'count': pit_plays.count(),
+ 'stats': []
+ }
+
+ for x in pit_plays:
+ this_dec = all_dec.where(Decision.pitcher == x.pitcher)
+ tot_outs = x.sum_outs if x.sum_outs > 0 else 1
+ obp = (x.sum_hit + x.sum_bb + x.sum_hbp + x.sum_ibb) / x.sum_pa
+ slg = (x.sum_hr * 4 + x.sum_triple * 3 + x.sum_double * 2 +
+ (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr)) / max(x.sum_ab, 1)
+ tot_bb = 0.1 if x.sum_bb == 0 else x.sum_bb
+
+ this_game = 'TOT'
+ if group_by in ['playergame', 'teamgame']:
+ this_game = x.game_id if short_output else model_to_dict(x.game, recurse=False)
+ this_dec = all_dec.where((Decision.pitcher == x.pitcher) & (Decision.game == x.game))
+
+ lob_all_rate, lob_2outs_rate, rbi_rate = 0, 0, 0
+ if x.count_runner1 + x.count_runner2 + x.count_runner3 > 0:
+ lob_all_rate = (x.count_lo1 + x.count_lo2 + x.count_lo3) / \
+ (x.count_runner1 + x.count_runner2 + x.count_runner3)
+ rbi_rate = (x.sum_rbi - x.sum_hr) / (x.count_runner1 + x.count_runner2 + x.count_runner3)
+
+ return_stats['stats'].append({
+ 'player': x.pitcher_id if short_output else model_to_dict(x.pitcher),
+ 'team': x.pitcher_team_id if short_output else model_to_dict(x.pitcher_team),
+ 'tbf': x.sum_pa,
+ 'outs': x.sum_outs,
+ 'games': this_dec[0].sum_game,
+ 'gs': this_dec[0].sum_gs,
+ 'win': this_dec[0].sum_win,
+ 'loss': this_dec[0].sum_loss,
+ 'hold': this_dec[0].sum_hold,
+ 'save': this_dec[0].sum_save,
+ 'bsave': this_dec[0].sum_b_save,
+ 'ir': this_dec[0].sum_irunners,
+ 'ir_sc': this_dec[0].sum_irun_scored,
+ 'ab': x.sum_ab,
+ 'run': x.sum_run,
+ 'e_run': x.sum_erun,
+ 'hits': x.sum_hit,
+ 'double': x.sum_double,
+ 'triple': x.sum_triple,
+ 'hr': x.sum_hr,
+ 'bb': x.sum_bb,
+ 'so': x.sum_so,
+ 'hbp': x.sum_hbp,
+ 'sac': x.sum_sac,
+ 'ibb': x.sum_ibb,
+ 'gidp': x.sum_gidp,
+ 'sb': x.sum_sb,
+ 'cs': x.sum_cs,
+ 'bphr': x.sum_bphr,
+ 'bpfo': x.sum_bpfo,
+ 'bp1b': x.sum_bp1b,
+ 'bplo': x.sum_bplo,
+ 'wp': x.sum_wp,
+ 'balk': x.sum_balk,
+ 'wpa': x.sum_wpa * -1,
+ 're24': x.sum_re24 * -1,
+ 'era': (x.sum_erun * 27) / tot_outs,
+ 'whip': ((x.sum_bb + x.sum_hit + x.sum_ibb) * 3) / tot_outs,
+ 'avg': x.sum_hit / max(x.sum_ab, 1),
+ 'obp': obp,
+ 'slg': slg,
+ 'ops': obp + slg,
+ 'woba': (.69 * x.sum_bb + .72 * x.sum_hbp + .89 * (x.sum_hit - x.sum_double - x.sum_triple - x.sum_hr) +
+ 1.27 * x.sum_double + 1.62 * x.sum_triple + 2.1 * x.sum_hr) / max(x.sum_pa - x.sum_ibb, 1),
+ 'k/9': x.sum_so * 9 / (tot_outs / 3),
+ 'bb/9': x.sum_bb * 9 / (tot_outs / 3),
+ 'k/bb': x.sum_so / tot_bb,
+ 'game': this_game,
+ 'lob_2outs': x.count_lo1_3out + x.count_lo2_3out + x.count_lo3_3out,
+ 'rbi%': rbi_rate
+ })
+ db.close()
+
+ if csv:
+ return_vals = return_stats['stats']
+ if len(return_vals) == 0:
+ return Response(content=pd.DataFrame().to_csv(index=False), media_type='text/csv')
+
+ for x in return_vals:
+ x['player_id'] = x['player']['player_id']
+ x['player_name'] = x['player']['p_name']
+ x['player_cardset'] = x['player']['cardset']['name']
+ x['team_id'] = x['team']['id']
+ x['team_abbrev'] = x['team']['abbrev']
+ del x['player'], x['team']
+
+ output = pd.DataFrame(return_vals)
+ first = ['player_id', 'player_name', 'player_cardset', 'team_id', 'team_abbrev']
+ exclude = first + ['lob_2outs', 'rbi%']
+ output = output[first + [col for col in output.columns if col not in exclude]]
+
+ db.close()
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+ return return_stats
+
+
+@router.get('/game-summary/{game_id}')
+async def get_game_summary(
+ game_id: int, csv: Optional[bool] = False, short_output: Optional[bool] = False, tp_max: Optional[int] = 1):
+ this_game = StratGame.get_or_none(StratGame.id == game_id)
+ if this_game is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Game {game_id} not found')
+
+ game_plays = StratPlay.select().where(StratPlay.game_id == game_id)
+ all_hits = game_plays.where(StratPlay.hit == 1)
+ all_errors = game_plays.where(StratPlay.error == 1)
+ all_runs = game_plays.where(StratPlay.run == 1)
+
+ all_dec = Decision.select().where(Decision.game_id == game_id)
+ winner = all_dec.where(Decision.win == 1)
+ loser = all_dec.where(Decision.loss == 1)
+ save_p = all_dec.where(Decision.is_save == 1)
+ save_pitcher = None
+ if save_p.count() > 0:
+ save_pitcher = model_to_dict(save_p.get().pitcher, recurse=not short_output)
+
+ all_holds = all_dec.where(Decision.hold == 1)
+ all_bsaves = all_dec.where(Decision.b_save == 1)
+
+ doubles = all_hits.where(StratPlay.double == 1)
+ triples = all_hits.where(StratPlay.triple == 1)
+ homeruns = all_hits.where((StratPlay.homerun == 1) | (StratPlay.bphr == 1))
+
+ steal_att = game_plays.where(StratPlay.runner.is_null(False))
+ all_sb = steal_att.where(StratPlay.sb == 1)
+ all_cs = steal_att.where(StratPlay.cs == 1)
+
+ top_batters = (
+ StratPlay
+ .select(StratPlay.batter, fn.SUM(StratPlay.re24).alias('sum_re24'), fn.SUM(StratPlay.ab).alias('sum_ab'),
+ fn.SUM(StratPlay.run).alias('sum_run'),
+ fn.SUM(StratPlay.hit).alias('sum_hit'), fn.SUM(StratPlay.rbi).alias('sum_rbi'),
+ fn.SUM(StratPlay.double).alias('sum_double'), fn.SUM(StratPlay.triple).alias('sum_triple'),
+ fn.SUM(StratPlay.homerun).alias('sum_hr'), fn.SUM(StratPlay.bphr).alias('sum_bphr'))
+ .where(StratPlay.game_id == game_id)
+ .group_by(StratPlay.batter)
+ .order_by(SQL('sum_re24').desc())
+ .limit(tp_max)
+ )
+ top_pitchers = (
+ StratPlay
+ .select(StratPlay.pitcher, fn.SUM(StratPlay.re24).alias('sum_re24'), fn.SUM(StratPlay.pa).alias('sum_pa'),
+ fn.SUM(StratPlay.outs).alias('sum_outs'), fn.SUM(StratPlay.e_run).alias('sum_erun'),
+ fn.SUM(StratPlay.run).alias('sum_run'), fn.SUM(StratPlay.so).alias('sum_so'),
+ fn.SUM(StratPlay.hit).alias('sum_hit'))
+ .where(StratPlay.game_id == game_id)
+ .group_by(StratPlay.pitcher)
+ .order_by(SQL('sum_re24').asc())
+ .limit(tp_max)
+ )
+ top_b = [{
+ 'player': model_to_dict(x.batter, recurse=not short_output),
+ 'ab': x.sum_ab,
+ 'run': x.sum_run,
+ 'hit': x.sum_hit,
+ 'rbi': x.sum_rbi,
+ 'double': x.sum_double,
+ 'triple': x.sum_triple,
+ 'hr': x.sum_hr + x.sum_bphr,
+ 're24': x.sum_re24
+ } for x in top_batters]
+ top_p = [{
+ 'player': model_to_dict(x.pitcher, recurse=not short_output),
+ 'tbf': x.sum_pa,
+ 'ip': math.floor(x.sum_outs / 3) + ((x.sum_outs % 3) * .1),
+ 'run': x.sum_run,
+ 'e_run': x.sum_erun,
+ 'hit': x.sum_hit,
+ 'so': x.sum_so,
+ 're24': x.sum_re24 * -1
+ } for x in top_pitchers]
+ top_players = [*top_b, *top_p]
+ logging.info(f'top_players: {top_players}')
+
+ return {
+ 'game': model_to_dict(this_game, recurse=not short_output),
+ 'teams': {
+ 'away': model_to_dict(this_game.away_team, recurse=not short_output),
+ 'home': model_to_dict(this_game.home_team, recurse=not short_output),
+ },
+ 'runs': {
+ 'away': all_runs.where(StratPlay.batter_team == this_game.away_team).count(),
+ 'home': all_runs.where(StratPlay.batter_team == this_game.home_team).count()
+ },
+ 'hits': {
+ 'away': all_hits.where(StratPlay.batter_team == this_game.away_team).count(),
+ 'home': all_hits.where(StratPlay.batter_team == this_game.home_team).count()
+ },
+ 'errors': {
+ 'away': all_errors.where(StratPlay.defender_team == this_game.away_team).count(),
+ 'home': all_errors.where(StratPlay.defender_team == this_game.home_team).count()
+ },
+ 'top-players': sorted(top_players, key=lambda x: x['re24'], reverse=True)[:tp_max],
+ 'pitchers': {
+ 'win': model_to_dict(winner.get().pitcher, recurse=not short_output),
+ 'loss': model_to_dict(loser.get().pitcher, recurse=not short_output),
+ 'holds': [model_to_dict(x.pitcher, recurse=not short_output) for x in all_holds],
+ 'save': save_pitcher,
+ 'b_saves': [model_to_dict(x.pitcher, recurse=not short_output) for x in all_bsaves]
+ },
+ 'xbh': {
+ '2b': [model_to_dict(x.batter, recurse=not short_output) for x in doubles],
+ '3b': [model_to_dict(x.batter, recurse=not short_output) for x in triples],
+ 'hr': [model_to_dict(x.batter, recurse=not short_output) for x in homeruns]
+ },
+ 'running': {
+ 'sb': [model_to_dict(x.runner, recurse=not short_output) for x in all_sb],
+ 'csc': [model_to_dict(x.catcher, recurse=not short_output) for x in all_cs]
+ }
+ }
+
+
+@router.get('/{play_id}')
+async def get_one_play(play_id: int):
+ if StratPlay.get_or_none(StratPlay.id == play_id) is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found')
+ r_play = model_to_dict(StratPlay.get_by_id(play_id))
+ db.close()
+ return r_play
+
+
+@router.patch('/{play_id}')
+async def patch_play(play_id: int, new_play: PlayModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'patch_play - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ if StratPlay.get_or_none(StratPlay.id == play_id) is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found')
+
+ StratPlay.update(**new_play.dict()).where(StratPlay.id == play_id).execute()
+ r_play = model_to_dict(StratPlay.get_by_id(play_id))
+ db.close()
+ return r_play
+
+
+@router.post('')
+async def post_plays(p_list: PlayList, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'post_plays - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ new_plays = []
+ this_game = StratGame.get_or_none(StratGame.id == p_list.plays[0].game_id)
+ if this_game is None:
+ raise HTTPException(status_code=404, detail=f'Game ID {p_list.plays[0].game_id} not found')
+
+ for play in p_list.plays:
+ this_play = play
+ this_play.inning_half = this_play.inning_half.lower()
+ top_half = this_play.inning_half == 'top'
+
+ if this_play.batter_team_id is None and this_play.batter_id is not None:
+ this_play.batter_team_id = this_game.away_team.id if top_half else this_game.home_team.id
+ if this_play.pitcher_team_id is None:
+ this_play.pitcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id
+ if this_play.catcher_id is not None:
+ this_play.catcher_team_id = this_game.home_team.id if top_half else this_game.away_team.id
+ if this_play.defender_id is not None:
+ this_play.defender_team_id = this_game.home_team.id if top_half else this_game.away_team.id
+ if this_play.runner_id is not None:
+ this_play.runner_team_id = this_game.away_team.id if top_half else this_game.home_team.id
+ if this_play.pa == 0:
+ this_play.batter_final = None
+
+ new_plays.append(this_play.dict())
+
+ with db.atomic():
+ for batch in chunked(new_plays, 20):
+ StratPlay.insert_many(batch).on_conflict_replace().execute()
+ db.close()
+
+ return f'Inserted {len(new_plays)} plays'
+
+
+@router.delete('/{play_id}')
+async def delete_play(play_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'delete_play - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_play = StratPlay.get_or_none(StratPlay.id == play_id)
+ if not this_play:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Play ID {play_id} not found')
+
+ count = this_play.delete_instance()
+ db.close()
+
+ if count == 1:
+ return f'Play {play_id} has been deleted'
+ else:
+ raise HTTPException(status_code=500, detail=f'Play {play_id} could not be deleted')
+
+
+@router.delete('/game/{game_id}')
+async def delete_plays_game(game_id: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'delete_plays_game - Bad Token: {token}')
+ raise HTTPException(status_code=401, detail='Unauthorized')
+
+ this_game = StratGame.get_or_none(StratGame.id == game_id)
+ if not this_game:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Game ID {game_id} not found')
+
+ count = StratPlay.delete().where(StratPlay.game == this_game).execute()
+ db.close()
+
+ if count > 0:
+ return f'Deleted {count} plays matching Game ID {game_id}'
+ else:
+ raise HTTPException(status_code=500, detail=f'No plays matching Game ID {game_id} were deleted')
+
diff --git a/app/routers_v2/teams.py b/app/routers_v2/teams.py
new file mode 100644
index 0000000..434960f
--- /dev/null
+++ b/app/routers_v2/teams.py
@@ -0,0 +1,997 @@
+from datetime import datetime
+
+import pandas as pd
+from fastapi import APIRouter, Depends, HTTPException, Response, Query
+from typing import Optional, Literal
+import logging
+import pydantic
+from pandas import DataFrame
+
+from ..db_engine import db, Team, model_to_dict, fn, Pack, Card, Player, Paperdex, Notification, PackType, \
+ Rarity, Current, query_to_csv, complex_data_to_csv, CARDSETS, CardPosition, BattingCardRatings, BattingCard, \
+ PitchingCard, PitchingCardRatings
+from ..dependencies import oauth2_scheme, valid_token, LOG_DATA, int_timestamp
+
+logging.basicConfig(
+ filename=LOG_DATA['filename'],
+ format=LOG_DATA['format'],
+ level=LOG_DATA['log_level']
+)
+
+router = APIRouter(
+ prefix='/api/v2/teams',
+ tags=['teams']
+)
+
+
+class TeamModel(pydantic.BaseModel):
+ abbrev: str
+ sname: str
+ lname: str
+ gmid: int
+ gmname: str
+ wallet: int = 0
+ gsheet: str
+ team_value: int = 0
+ collection_value: int = 0
+ logo: Optional[str] = None
+ color: Optional[str] = None
+ season: int
+ ps_shiny: Optional[int] = 0
+ ranking: Optional[int] = 1000
+ has_guide: Optional[bool] = False
+ is_ai: Optional[bool] = False
+
+
+@router.get('')
+async def get_teams(
+ season: Optional[int] = None, gm_id: Optional[int] = None, abbrev: Optional[str] = None,
+ tv_min: Optional[int] = None, tv_max: Optional[int] = None, cv_min: Optional[int] = None,
+ cv_max: Optional[int] = None, ps_shiny_min: Optional[int] = None, ps_shiny_max: Optional[int] = None,
+ ranking_min: Optional[int] = None, ranking_max: Optional[int] = None, has_guide: Optional[bool] = None,
+ sname: Optional[str] = None, lname: Optional[str] = None, is_ai: Optional[bool] = None,
+ event_id: Optional[int] = None, limit: Optional[int] = None, csv: Optional[bool] = False):
+ """
+ Param: season: int
+ Param: team_abbrev: string
+ Param: owner_id: int
+ """
+ if season:
+ all_teams = Team.select_season(season)
+ else:
+ all_teams = Team.select()
+
+ # if all_teams.count() == 0:
+ # db.close()
+ # raise HTTPException(status_code=404, detail=f'There are no teams to filter')
+
+ if gm_id is not None:
+ all_teams = all_teams.where(Team.gmid == gm_id)
+
+ if abbrev is not None:
+ all_teams = all_teams.where(fn.Lower(Team.abbrev) == abbrev.lower())
+
+ if sname is not None:
+ all_teams = all_teams.where(fn.Lower(Team.sname) == sname.lower())
+
+ if lname is not None:
+ all_teams = all_teams.where(fn.Lower(Team.lname) == lname.lower())
+
+ if tv_min is not None:
+ all_teams = all_teams.where(Team.team_value >= tv_min)
+
+ if tv_max is not None:
+ all_teams = all_teams.where(Team.team_value <= tv_max)
+
+ if cv_min is not None:
+ all_teams = all_teams.where(Team.collection_value >= cv_min)
+
+ if cv_max is not None:
+ all_teams = all_teams.where(Team.collection_value <= cv_max)
+
+ if ps_shiny_min is not None:
+ all_teams = all_teams.where(Team.career >= ps_shiny_min)
+
+ if ps_shiny_max is not None:
+ all_teams = all_teams.where(Team.career <= ps_shiny_max)
+
+ if ranking_min is not None:
+ all_teams = all_teams.where(Team.ranking >= ranking_min)
+
+ if ranking_max is not None:
+ all_teams = all_teams.where(Team.ranking <= ranking_max)
+
+ if ranking_max is not None:
+ all_teams = all_teams.where(Team.ranking <= ranking_max)
+
+ if has_guide is not None:
+ if not has_guide:
+ all_teams = all_teams.where(Team.has_guide == 0)
+ else:
+ all_teams = all_teams.where(Team.has_guide == 1)
+
+ if is_ai is not None:
+ all_teams = all_teams.where(Team.is_ai)
+
+ if event_id is not None:
+ all_teams = all_teams.where(Team.event_id == event_id)
+
+ if limit is not None:
+ all_teams = all_teams.limit(limit)
+
+ if csv:
+ return_val = query_to_csv(all_teams, exclude=[Team.career])
+ db.close()
+ return Response(content=return_val, media_type='text/csv')
+
+ else:
+ return_teams = {'count': all_teams.count(), 'teams': []}
+ for x in all_teams:
+ return_teams['teams'].append(model_to_dict(x))
+
+ db.close()
+ return return_teams
+
+
+@router.get('/{team_id}')
+async def get_one_team(team_id, inc_packs: bool = True, csv: Optional[bool] = False):
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ p_query = Pack.select().where((Pack.team == this_team) & (Pack.open_time.is_null(True)))
+ if csv:
+ data = model_to_dict(this_team)
+ data['sealed_packs'] = p_query.count()
+ return_val = complex_data_to_csv([data])
+ else:
+ return_val = model_to_dict(this_team)
+ if inc_packs:
+ return_val['sealed_packs'] = [model_to_dict(x) for x in p_query]
+
+ db.close()
+ return return_val
+
+
+@router.get('/{team_id}/lineup/{difficulty_name}')
+async def get_team_lineup(team_id: int, difficulty_name: str, pitcher_name: str, d_rank: int = 5, o_rank: int = 5):
+ """
+ d_rank: int - 10: best overall, 9: prioritize range, 8: prioritize error
+ """
+ this_team = Team.get_or_none(Team.id == team_id)
+ if this_team is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Team id {team_id} not found')
+
+ if difficulty_name not in CARDSETS.keys():
+ db.close()
+ raise HTTPException(status_code=400, detail=f'Difficulty name {difficulty_name} not a valid check')
+
+ # all_players = Player.select().where(
+ # (fn.Lower(Player.p_name) != pitcher_name.lower()) & (Player.mlbclub == this_team.lname)
+ # )
+ all_players = Player.select().where(Player.mlbclub == this_team.lname)
+
+ legal_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['primary'])
+ if 'secondary' in CARDSETS[difficulty_name]:
+ backup_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['secondary'])
+ else:
+ backup_players = None
+
+ logging.info(f'legal_players: {legal_players.count()}')
+ if backup_players is not None:
+ logging.info(f'backup_players: {backup_players.count()}')
+ player_names = []
+ starting_nine = {
+ 'C': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ '1B': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ '2B': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ '3B': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ 'SS': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ 'LF': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ 'CF': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ 'RF': {'player': None, 'vl': None, 'vr': None, 'ops': 0},
+ 'DH': {'player': None, 'vl': None, 'vr': None, 'ops': 0}
+ }
+
+ def get_bratings(player_id):
+ this_bcard = BattingCard.get_or_none(BattingCard.player_id == player_id)
+ vl_ratings = BattingCardRatings.get_or_none(
+ BattingCardRatings.battingcard == this_bcard, BattingCardRatings.vs_hand == 'L'
+ )
+ vl_ops = vl_ratings.obp + vl_ratings.slg
+ vr_ratings = BattingCardRatings.get_or_none(
+ BattingCardRatings.battingcard == this_bcard, BattingCardRatings.vs_hand == 'R'
+ )
+ vr_ops = vr_ratings.obp + vr_ratings.slg
+ return model_to_dict(vl_ratings), model_to_dict(vr_ratings), (vl_ops + vr_ops + min(vl_ops, vr_ops)) / 3
+
+ # IDEA: Rank guys by their bat per-position and take the best one that meets a threshold of defensive ability
+ for position in starting_nine.keys():
+ if position == 'DH':
+ # all_bcards = BattingCard.select().where(BattingCard.player << legal_players)
+ # all_batters = BattingCardRatings.select().where(
+ # BattingCardRatings.battingcard << all_bcards
+ # ).order_by(BattingCardRatings.obp + BattingCardRatings.sl)
+ #
+ # for x in all_batters:
+ # if x.battingcard.player.p_name not in player_names:
+ # starting_nine['DH'] = x.battingcard.player
+ # break
+ logging.info(f'Searching for a DH!')
+ dh_query = legal_players.order_by(Player.cost.desc())
+ for x in dh_query:
+ logging.info(f'checking {x.p_name} for {position}')
+ if x.p_name not in player_names and 'P' not in x.pos_1:
+ logging.info(f'adding!')
+ starting_nine['DH']['player'] = model_to_dict(x)
+ try:
+ vl, vr, total_ops = get_bratings(x.player_id)
+ except AttributeError as e:
+ logging.info(f'Could not find batting lines')
+ else:
+ starting_nine['DH']['vl'] = vl
+ starting_nine['DH']['vr'] = vr
+ starting_nine['DH']['ops'] = total_ops
+ player_names.append(x.p_name)
+ break
+
+ if starting_nine['DH']['player'] is None:
+ dh_query = backup_players.order_by(Player.cost.desc())
+ for x in dh_query:
+ logging.info(f'checking {x.p_name} for {position}')
+ if x.p_name not in player_names:
+ logging.info(f'adding!')
+ starting_nine['DH']['player'] = model_to_dict(x)
+ try:
+ vl, vr, total_ops = get_bratings(x.player_id)
+ except AttributeError as e:
+ logging.info(f'Could not find batting lines')
+ else:
+ vl, vr, total_ops = get_bratings(x.player_id)
+ starting_nine['DH']['vl'] = vl
+ starting_nine['DH']['vr'] = vr
+ starting_nine['DH']['ops'] = total_ops
+ player_names.append(x.p_name)
+ break
+
+ else:
+ pos_group = CardPosition.select().where(
+ (CardPosition.position == position) & (CardPosition.player << legal_players)
+ )
+ backup_group = CardPosition.select().where(
+ (CardPosition.position == position) & (CardPosition.player << backup_players)
+ )
+ if difficulty_name in ['minor-league', 'gauntlet-3']:
+ pos_group = pos_group.order_by(CardPosition.innings.desc())
+ elif d_rank == 10:
+ pos_group = pos_group.order_by((CardPosition.range * 5) + CardPosition.error)
+ elif d_rank == 9:
+ pos_group = pos_group.order_by(CardPosition.range)
+ elif d_rank == 8:
+ pos_group = pos_group.order_by(CardPosition.error.desc())
+
+ logging.info(f'pos_group: {pos_group}\n{starting_nine}\n{player_names}\n\n')
+ if difficulty_name in ['minor-league', 'gauntlet-3']:
+ for x in pos_group:
+ logging.info(f'checking {x.player.p_name} for {position}')
+ if x.player.p_name not in player_names and x.player.p_name.lower() != pitcher_name:
+ logging.info(f'adding!')
+ starting_nine[position]['player'] = model_to_dict(x.player)
+ vl, vr, total_ops = get_bratings(x.player.player_id)
+ starting_nine[position]['vl'] = vl
+ starting_nine[position]['vr'] = vr
+ starting_nine[position]['ops'] = total_ops
+ player_names.append(x.player.p_name)
+ break
+
+ if starting_nine[position]['player'] is None:
+ for x in backup_group:
+ logging.info(f'checking {x.player.p_name} for {position}')
+ if x.player.p_name not in player_names and x.player.p_name.lower() != pitcher_name:
+ logging.info(f'adding!')
+ starting_nine[position]['player'] = model_to_dict(x.player)
+ vl, vr, total_ops = get_bratings(x.player.player_id)
+ starting_nine[position]['vl'] = vl
+ starting_nine[position]['vr'] = vr
+ starting_nine[position]['ops'] = total_ops
+ player_names.append(x.player.p_name)
+ break
+
+ # all_bcards = BattingCard.select().where(BattingCard.player << starting_nine.values())
+ # all_ratings = BattingCardRatings.select().where(BattingCardRatings.battingcard << all_bcards)
+ #
+ # vl_query = all_ratings.where(BattingCardRatings.vs_hand == 'L')
+ # vr_query = all_ratings.where(BattingCardRatings.vs_hand == 'R')
+ #
+ # vl_vals = [model_to_dict(x) for x in vl_query]
+ # for x in vl_vals:
+ # x.update(x['battingcard'])
+ # x['player_id'] = x['battingcard']['player']['player_id']
+ # x['player_name'] = x['battingcard']['player']['p_name']
+ # x['rarity'] = x['battingcard']['player']['rarity']['name']
+ # x['cardset_id'] = x['battingcard']['player']['cardset']['id']
+ # x['cardset_name'] = x['battingcard']['player']['cardset']['name']
+ # del x['player']
+ #
+ # vr_vals = [model_to_dict(x) for x in vr_query]
+ # for x in vr_vals:
+ # x['player_id'] = x['battingcard']['player']['player_id']
+ # del x['battingcard']
+ #
+ # vl = pd.DataFrame(vl_vals)
+ # vr = pd.DataFrame(vr_vals)
+ # db.close()
+ #
+ # output = pd.merge(vl, vr, on='player_id', suffixes=('_vl', '_vr'))
+ #
+ # def get_total_ops(df_data):
+ # ops_vl = df_data['obp_vL'] + df_data['slg_vL']
+ # ops_vr = df_data['obp_vR'] + df_data['slg_vR']
+ # return (ops_vr + ops_vl + min(ops_vl, ops_vr)) / 3
+ # output['total_OPS'] = output.apply(get_total_ops, axis=1)
+ # output = output.sort_values(by=['total_OPS'], ascending=False)
+
+ sorted_nine = sorted(starting_nine.items(), key=lambda item: item[1]['ops'], reverse=True)
+ return {
+ 'json': dict(sorted_nine),
+ 'array': sorted_nine
+ }
+
+
+def sort_pitchers(pitching_card_query) -> DataFrame | None:
+ all_s = [model_to_dict(x, recurse=False) for x in pitching_card_query]
+ if len(all_s) == 0:
+ logging.error(f'Empty pitching_card_query: {pitching_card_query}')
+ return None
+
+ pitcher_df = pd.DataFrame(all_s).set_index('player', drop=False)
+ logging.info(f'pitcher_df: {pitcher_df}')
+
+ def get_total_ops(df_data):
+ vlval = PitchingCardRatings.get_or_none(
+ PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'L')
+ vrval = PitchingCardRatings.get_or_none(
+ PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'R')
+
+ ops_vl = vlval.obp + vlval.slg
+ ops_vr = vrval.obp + vrval.slg
+ return (ops_vr + ops_vl + min(ops_vl, ops_vr)) / 3
+
+ pitcher_df['total_ops'] = pitcher_df.apply(get_total_ops, axis=1)
+ return pitcher_df.sort_values(by='total_ops')
+
+
+@router.get('/{team_id}/sp/{difficulty_name}')
+async def get_team_sp(team_id: int, difficulty_name: str, sp_rank: int):
+ logging.info(f'get_team_sp - team_id: {team_id} / difficulty_name: {difficulty_name} / sp_rank: {sp_rank}')
+ this_team = Team.get_or_none(Team.id == team_id)
+ if this_team is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Team id {team_id} not found')
+
+ if difficulty_name not in CARDSETS.keys():
+ db.close()
+ raise HTTPException(status_code=400, detail=f'Difficulty name {difficulty_name} not a valid check')
+
+ all_players = Player.select().where(Player.mlbclub == this_team.lname)
+
+ legal_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['primary'])
+ if 'secondary' in CARDSETS[difficulty_name]:
+ backup_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['secondary'])
+ else:
+ backup_players = None
+
+ def sort_starters(starter_query) -> DataFrame | None:
+ all_s = [model_to_dict(x, recurse=False) for x in starter_query]
+ if len(all_s) == 0:
+ logging.error(f'Empty starter_query: {starter_query}')
+ return None
+
+ starter_df = pd.DataFrame(all_s).set_index('player', drop=False)
+ logging.debug(f'starter_df: {starter_df}')
+
+ def get_total_ops(df_data):
+ vlval = PitchingCardRatings.get_or_none(
+ PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'L')
+ vrval = PitchingCardRatings.get_or_none(
+ PitchingCardRatings.pitchingcard_id == df_data['id'], PitchingCardRatings.vs_hand == 'R')
+
+ ops_vl = vlval.obp + vlval.slg
+ ops_vr = vrval.obp + vrval.slg
+ return (ops_vr + ops_vl + min(ops_vl, ops_vr)) / 3
+
+ starter_df['total_ops'] = starter_df.apply(get_total_ops, axis=1)
+ return starter_df.sort_values(by='total_ops')
+
+ # Find SP in primary cardsets
+ s_query = PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.starter_rating >= 4)
+ )
+ all_starters = sort_starters(s_query)
+ logging.debug(f'sorted: {all_starters}')
+
+ if all_starters is not None and len(all_starters.index) >= sp_rank:
+ this_player_id = all_starters.iloc[sp_rank - 1].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ if all_starters is not None and len(all_starters.index) > 0:
+ this_player_id = all_starters.iloc[len(all_starters.index) - 1].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ # Include backup cardsets
+ s_query = PitchingCard.select().where(
+ (PitchingCard.player << backup_players) & (PitchingCard.starter_rating >= 4)
+ )
+ all_starters = sort_starters(s_query)
+ logging.debug(f'sorted: {all_starters}')
+
+ if all_starters is not None and len(all_starters.index) >= sp_rank:
+ this_player_id = all_starters.iloc[sp_rank - 1].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ if all_starters is not None and len(all_starters.index) > 0:
+ this_player_id = all_starters.iloc[len(all_starters.index) - 1].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ raise HTTPException(status_code=500, detail=f'No SP #{sp_rank} found for Team {team_id}')
+
+
+@router.get('/{team_id}/rp/{difficulty_name}')
+async def get_team_rp(
+ team_id: int, difficulty_name: str, need: Literal['length', 'setup', 'closer', 'middle'],
+ used_pitcher_ids: list = Query(default=[])):
+ logging.info(f'get_team_rp - team_id: {team_id} / difficulty_name: {difficulty_name} / need: {need} '
+ f'/ used_pitcher_ids: {used_pitcher_ids}')
+ this_team = Team.get_or_none(Team.id == team_id)
+ if this_team is None:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'Team id {team_id} not found')
+
+ if difficulty_name not in CARDSETS.keys():
+ db.close()
+ raise HTTPException(status_code=400, detail=f'Difficulty name {difficulty_name} not a valid check')
+
+ all_players = Player.select().where(
+ (Player.mlbclub == this_team.lname) & (Player.player_id.not_in(used_pitcher_ids))
+ )
+
+ legal_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['primary'])
+ if 'secondary' in CARDSETS[difficulty_name]:
+ backup_players = all_players.where(Player.cardset_id << CARDSETS[difficulty_name]['secondary'])
+ else:
+ backup_players = None
+
+ if need == 'closer':
+ for query in [PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.closer_rating >= 3) &
+ (PitchingCard.starter_rating == 1)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.closer_rating >= 1) &
+ (PitchingCard.starter_rating == 1)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) & (PitchingCard.closer_rating >= 3) &
+ (PitchingCard.starter_rating == 1)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) & (PitchingCard.closer_rating >= 1) &
+ (PitchingCard.starter_rating == 1)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) & (PitchingCard.starter_rating < 4))
+ ]:
+ all_relievers = sort_pitchers(query)
+
+ if all_relievers is not None:
+ this_player_id = all_relievers.iloc[0].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ elif need == 'setup':
+ for query in [PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.starter_rating == 1)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) & (PitchingCard.starter_rating < 4))
+ ]:
+ all_relievers = sort_pitchers(query)
+
+ if all_relievers is not None and len(all_relievers.index) >= 2:
+ this_player_id = all_relievers.iloc[1].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ elif need == 'length' or len(used_pitcher_ids) > 4:
+ for query in [PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.relief_rating >= 3) &
+ (PitchingCard.starter_rating < 4)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.relief_rating >= 2) &
+ (PitchingCard.starter_rating < 4)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) & (PitchingCard.relief_rating >= 2) &
+ (PitchingCard.starter_rating < 4))
+ ]:
+ all_relievers = sort_pitchers(query)
+
+ if all_relievers is not None:
+ this_player_id = all_relievers.iloc[0].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ elif need == 'middle':
+ for query in [PitchingCard.select().join(Player).where(
+ (PitchingCard.player << legal_players) & (PitchingCard.starter_rating == 1)),
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) & (PitchingCard.starter_rating < 4))
+ ]:
+ all_relievers = sort_pitchers(query)
+
+ if all_relievers is not None and len(all_relievers.index) >= 3:
+ this_player_id = all_relievers.iloc[2].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+ logging.info(f'Falling to last chance pitcher')
+ all_relievers = sort_pitchers(
+ PitchingCard.select().join(Player).where(
+ (PitchingCard.player << backup_players) | (PitchingCard.player << legal_players)
+ )
+ )
+
+ if all_relievers is not None:
+ this_player_id = all_relievers.iloc[len(all_relievers.index) - 1].player
+ this_player = model_to_dict(Player.get_by_id(this_player_id), recurse=False)
+ db.close()
+ return this_player
+
+
+@router.get('/{team_id}/buy/players')
+async def team_buy_players(team_id: int, ids: str, ts: str):
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ if ts != this_team.team_hash():
+ logging.warning(f'Bad Team Secret: {ts} ({this_team.team_hash()})')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail=f'You are not authorized to buy {this_team.abbrev} cards. This event has been logged.'
+ )
+
+ last_card = Card.select(Card.id).order_by(-Card.id).limit(1)
+ lc_id = last_card[0].id
+
+ all_ids = ids.split(',')
+ conf_message = ''
+ total_cost = 0
+ for player_id in all_ids:
+ if player_id != '':
+ try:
+ this_player = Player.get_by_id(player_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No player found with id {player_id} /// '
+ f'{conf_message} purchased')
+
+ # check wallet balance
+ if this_team.wallet < this_player.cost:
+ logging.info(f'{this_player} was not purchased. {this_team.lname} only has {this_team.wallet}₼, but '
+ f'{this_player} costs {this_player.cost}₼.')
+ db.close()
+ raise HTTPException(
+ 200,
+ detail=f'{this_player} was not purchased. {this_team.lname} only has {this_team.wallet}₼, but '
+ f'{this_player} costs {this_player.cost}₼. /// {conf_message} purchased'
+ )
+
+ # Create player card and update cost
+ buy_price = this_player.cost
+ total_cost += buy_price
+ this_card = Card(
+ player_id=this_player.player_id,
+ team_id=this_team.id,
+ value=buy_price
+ )
+ Paperdex.get_or_create(team_id=team_id, player_id=this_player.player_id)
+ this_card.save()
+ this_player.change_on_buy()
+
+ # Deduct card cost from team
+ logging.info(f'{this_team.abbrev} starting wallet: {this_team.wallet}')
+ this_team.wallet -= buy_price
+ this_team.save()
+ logging.info(f'{this_team.abbrev} ending wallet: {this_team.wallet}')
+
+ # Post a notification
+ if this_player.rarity.value >= 2:
+ new_notif = Notification(
+ created=int_timestamp(datetime.now()),
+ title=f'Price Change',
+ desc='Modified by buying and selling',
+ field_name=f'{this_player.description} '
+ f'{this_player.p_name if this_player.p_name not in this_player.description else ""}',
+ message=f'From {buy_price}₼ 📈 to **{this_player.cost}**₼',
+ about=f'Player-{this_player.player_id}'
+ )
+ new_notif.save()
+
+ conf_message += f'{buy_price}₼ for {this_player.rarity.name} {this_player.p_name} ' \
+ f'({this_player.cardset.name}), '
+
+ # sheets.post_new_cards(SHEETS_AUTH, lc_id)
+
+ raise HTTPException(status_code=200, detail=f'{conf_message} purchased. /// Total Cost: {total_cost}₼ /// '
+ f'Final Wallet: {this_team.wallet}')
+
+
+@router.get('/{team_id}/buy/pack/{packtype_id}')
+async def team_buy_packs(team_id: int, packtype_id: int, ts: str, quantity: Optional[int] = 1):
+ try:
+ this_packtype = PackType.get_by_id(packtype_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No pack type found with id {packtype_id}')
+
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ if ts != this_team.team_hash():
+ logging.warning(f'Bad Team Secret: {ts} ({this_team.team_hash()})')
+ db.close()
+ logging.warning(f'team: {this_team} / pack_type: {this_packtype} / secret: {ts} / '
+ f'actual: {this_team.team_hash()}')
+ raise HTTPException(
+ status_code=401,
+ detail=f'You are not authorized to buy {this_team.abbrev} packs. This event has been logged.'
+ )
+
+ # check wallet balance
+ total_cost = this_packtype.cost * quantity
+ if this_team.wallet < total_cost:
+ db.close()
+ raise HTTPException(
+ 200,
+ detail=f'{this_packtype} was not purchased. {this_team.lname} only has {this_team.wallet} bucks, but '
+ f'{this_packtype} costs {this_packtype.cost}.'
+ )
+
+ all_packs = []
+ for i in range(quantity):
+ all_packs.append(Pack(team_id=this_team.id, pack_type_id=this_packtype.id))
+
+ # Deduct card cost from team
+ logging.info(f'{this_team.abbrev} starting wallet: {this_team.wallet}')
+ this_team.wallet -= total_cost
+ this_team.save()
+ logging.info(f'{this_team.abbrev} ending wallet: {this_team.wallet}')
+
+ with db.atomic():
+ Pack.bulk_create(all_packs, batch_size=15)
+ db.close()
+
+ raise HTTPException(
+ status_code=200,
+ detail=f'Quantity {quantity} {this_packtype.name} pack{"s" if quantity > 1 else ""} have been purchased by '
+ f'{this_team.lname} for {total_cost} bucks. You may close this window.'
+ )
+
+
+@router.get('/{team_id}/sell/cards')
+async def team_sell_cards(team_id: int, ids: str, ts: str):
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ if ts != this_team.team_hash():
+ logging.warning(f'Bad Team Secret: {ts} ({this_team.team_hash()})')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail=f'You are not authorized to sell {this_team.abbrev} cards. This event has been logged.'
+ )
+
+ all_ids = ids.split(',')
+ del_ids = []
+ conf_message = ''
+ total_cost = 0
+ for card_id in all_ids:
+ if card_id != '':
+ try:
+ this_card = Card.get_by_id(card_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No card found with id {card_id}')
+
+ del_ids.append(card_id)
+ this_player = this_card.player
+
+ if this_card.team != this_team:
+ raise HTTPException(status_code=401,
+ detail=f'Card id {card_id} ({this_player.p_name}) belongs to '
+ f'{this_card.team.abbrev} and cannot be sold. /// {conf_message} sold')
+
+ orig_price = this_player.cost
+ sell_price = round(this_player.cost * .5)
+ total_cost += sell_price
+
+ # credit selling team's wallet
+ if this_team.wallet is None:
+ this_team.wallet = sell_price
+ else:
+ this_team.wallet += sell_price
+ this_team.save()
+
+ # decrease price of player
+ this_player.change_on_sell()
+ this_card.delete_instance()
+
+ # post a notification
+ if this_player.rarity.value >= 2:
+ new_notif = Notification(
+ created=int_timestamp(datetime.now()),
+ title=f'Price Change',
+ desc='Modified by buying and selling',
+ field_name=f'{this_player.description} '
+ f'{this_player.p_name if this_player.p_name not in this_player.description else ""}',
+ message=f'From {orig_price}₼ 📉 to **{this_player.cost}**₼',
+ about=f'Player-{this_player.id}'
+ )
+ new_notif.save()
+
+ conf_message += f'{sell_price}₼ for {this_player.rarity.name} {this_player.p_name} ' \
+ f'({this_player.cardset.name}), '
+
+ # sheets.post_deletion(SHEETS_AUTH, del_ids)
+ raise HTTPException(status_code=200, detail=f'{conf_message} sold. /// Total Earned: {total_cost}₼ /// '
+ f'Final Wallet: {this_team.wallet}')
+
+
+@router.get('/{team_id}/cards')
+async def get_team_cards(team_id, csv: Optional[bool] = True):
+ """
+ CSV output specifically targeting team roster sheet
+
+ Parameters
+ ----------
+ team_id
+ csv
+ """
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ if not csv:
+ db.close()
+ raise HTTPException(
+ status_code=400,
+ detail='The /teams/{team_id}/cards endpoint only supports csv output.'
+ )
+
+ all_cards = (Card
+ .select()
+ .join(Player)
+ .join(Rarity)
+ .where(Card.team == this_team)
+ .order_by(-Card.player.rarity.value, Card.player.p_name)
+ )
+ if all_cards.count() == 0:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No cards found')
+
+ card_vals = [model_to_dict(x) for x in all_cards]
+ db.close()
+
+ for x in card_vals:
+ x.update(x['player'])
+ x['player_id'] = x['player']['player_id']
+ x['player_name'] = x['player']['p_name']
+ x['cardset_id'] = x['player']['cardset']['id']
+ x['cardset_name'] = x['player']['cardset']['name']
+ x['rarity'] = x['player']['rarity']['name']
+ x['card_id'] = x['id']
+
+ card_df = pd.DataFrame(card_vals)
+ output = card_df[[
+ 'cardset_name', 'player_name', 'rarity', 'image', 'image2', 'pos_1', 'pos_2', 'pos_3', 'pos_4', 'pos_5',
+ 'pos_6', 'pos_7', 'pos_8', 'cost', 'mlbclub', 'franchise', 'fangr_id', 'bbref_id', 'player_id', 'card_id']]
+ return Response(content=pd.DataFrame(output).to_csv(index=False), media_type='text/csv')
+
+
+@router.post('')
+async def post_team(team: TeamModel, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post teams. This event has been logged.'
+ )
+
+ dupe_team = Team.get_or_none(Team.season == team.season, Team.abbrev == team.abbrev)
+ if dupe_team:
+ db.close()
+ raise HTTPException(status_code=400, detail=f'There is already a season {team.season} team using {team.abbrev}')
+
+ this_team = Team(
+ abbrev=team.abbrev,
+ sname=team.sname,
+ lname=team.lname,
+ gmid=team.gmid,
+ gmname=team.gmname,
+ wallet=team.wallet,
+ gsheet=team.gsheet,
+ team_value=team.team_value,
+ collection_value=team.collection_value,
+ logo=team.logo,
+ color=team.color,
+ ranking=team.ranking,
+ season=team.season,
+ career=team.ps_shiny,
+ has_guide=team.has_guide,
+ is_ai=team.is_ai
+ )
+
+ saved = this_team.save()
+ if saved == 1:
+ return_team = model_to_dict(this_team)
+ db.close()
+ return return_team
+ else:
+ raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team')
+
+
+@router.post('/new-season/{new_season}')
+async def team_season_update(new_season: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to post teams. This event has been logged.'
+ )
+
+ r_query = Team.update(ranking=1000, season=new_season, wallet=Team.wallet + 250).execute()
+ current = Current.latest()
+ current.season = new_season
+ current.save()
+ db.close()
+
+ return {'detail': f'Team rankings, season, and wallet updated for season {new_season}'}
+
+
+@router.post('/{team_id}/money/{delta}')
+async def team_update_money(team_id: int, delta: int, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to adjust wallets. This event has been logged.'
+ )
+
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ this_team.wallet += delta
+
+ if this_team.save() == 1:
+ return_team = model_to_dict(this_team)
+ db.close()
+ return return_team
+ else:
+ raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team')
+
+
+@router.patch('/{team_id}')
+async def patch_team(
+ team_id, sname: Optional[str] = None, lname: Optional[str] = None, gmid: Optional[int] = None,
+ gmname: Optional[str] = None, gsheet: Optional[str] = None, team_value: Optional[int] = None,
+ collection_value: Optional[int] = None, logo: Optional[str] = None, color: Optional[str] = None,
+ season: Optional[int] = None, ps_shiny: Optional[int] = None, wallet_delta: Optional[int] = None,
+ has_guide: Optional[bool] = None, is_ai: Optional[bool] = None, ranking: Optional[int] = None,
+ token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete teams. This event has been logged.'
+ )
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ if sname is not None:
+ this_team.sname = sname
+ if lname is not None:
+ this_team.lname = lname
+ if gmid is not None:
+ this_team.gmid = gmid
+ if gmname is not None:
+ this_team.gmname = gmname
+ if gsheet is not None:
+ this_team.gsheet = gsheet
+ if team_value is not None:
+ this_team.team_value = team_value
+ if collection_value is not None:
+ this_team.collection_value = collection_value
+ if logo is not None:
+ this_team.logo = logo
+ if color is not None:
+ this_team.color = color
+ if season is not None:
+ this_team.season = season
+ if ps_shiny is not None:
+ this_team.career = ps_shiny
+ if ranking is not None:
+ this_team.ranking = ranking
+ if wallet_delta is not None:
+ this_team.wallet += wallet_delta
+ if has_guide is not None:
+ if has_guide:
+ this_team.has_guide = 1
+ else:
+ this_team.has_guide = 0
+ if is_ai is not None:
+ if is_ai:
+ this_team.is_ai = 1
+ else:
+ this_team.is_ai = 0
+
+ if this_team.save() == 1:
+ return_team = model_to_dict(this_team)
+ db.close()
+ return return_team
+ else:
+ raise HTTPException(status_code=418, detail='Well slap my ass and call me a teapot; I could not save that team')
+
+
+@router.delete('/{team_id}')
+async def delete_team(team_id, token: str = Depends(oauth2_scheme)):
+ if not valid_token(token):
+ logging.warning(f'Bad Token: {token}')
+ db.close()
+ raise HTTPException(
+ status_code=401,
+ detail='You are not authorized to delete teams. This event has been logged.'
+ )
+ try:
+ this_team = Team.get_by_id(team_id)
+ except Exception:
+ db.close()
+ raise HTTPException(status_code=404, detail=f'No team found with id {team_id}')
+
+ count = this_team.delete_instance()
+ db.close()
+
+ if count == 1:
+ raise HTTPException(status_code=200, detail=f'Team {team_id} has been deleted')
+ else:
+ raise HTTPException(status_code=500, detail=f'Team {team_id} was not deleted')
diff --git a/db_engine.py b/db_engine.py
index e106c64..7eb3cc2 100644
--- a/db_engine.py
+++ b/db_engine.py
@@ -1,9 +1,13 @@
import math
from datetime import datetime
+from typing import List
import logging
import os
+from pandas import DataFrame
from peewee import *
+from peewee import ModelSelect
+from playhouse.shortcuts import model_to_dict
db = SqliteDatabase(
'storage/pd_master.db',
@@ -23,6 +27,62 @@ logging.basicConfig(
)
+def model_csv_headers(this_obj, exclude=None) -> List:
+ data = model_to_dict(this_obj, recurse=False, exclude=exclude)
+ return [x for x in data.keys()]
+
+
+def model_to_csv(this_obj, exclude=None) -> List:
+ data = model_to_dict(this_obj, recurse=False, exclude=exclude)
+ return [x for x in data.values()]
+
+
+def query_to_csv(all_items: ModelSelect, exclude=None):
+ if all_items.count() == 0:
+ data_list = [['No data found']]
+ else:
+ data_list = [model_csv_headers(all_items[0], exclude=exclude)]
+ for x in all_items:
+ data_list.append(model_to_csv(x, exclude=exclude))
+
+ return DataFrame(data_list).to_csv(header=False, index=False)
+
+
+def complex_data_to_csv(complex_data: List):
+ if len(complex_data) == 0:
+ data_list = [['No data found']]
+ else:
+ data_list = [[x for x in complex_data[0].keys()]]
+ for line in complex_data:
+ logging.debug(f'line: {line}')
+ this_row = []
+ for key in line:
+ logging.debug(f'key: {key}')
+ if line[key] is None:
+ this_row.append('')
+
+ elif isinstance(line[key], dict):
+ if 'name' in line[key]:
+ this_row.append(line[key]['name'])
+ elif 'abbrev' in line[key]:
+ this_row.append(line[key]['abbrev'])
+ else:
+ this_row.append(line[key]['id'])
+
+ elif isinstance(line[key], int) and line[key] > 100000000:
+ this_row.append(f"'{line[key]}")
+
+ elif isinstance(line[key], str) and ',' in line[key]:
+ this_row.append(line[key].replace(",", "-_-"))
+
+ else:
+ this_row.append(line[key])
+
+ data_list.append(this_row)
+
+ return DataFrame(data_list).to_csv(header=False, index=False)
+
+
class BaseModel(Model):
class Meta:
database = db
@@ -84,6 +144,19 @@ class Cardset(BaseModel):
db.create_tables([Cardset])
+class MlbPlayer(BaseModel):
+ first_name = CharField()
+ last_name = CharField()
+ key_fangraphs = IntegerField(null=True)
+ key_bbref = CharField(null=True)
+ key_retro = CharField(null=True)
+ key_mlbam = IntegerField(null=True)
+ offense_col = IntegerField(default=1)
+
+
+db.create_tables([MlbPlayer])
+
+
class Player(BaseModel):
player_id = IntegerField(primary_key=True)
p_name = CharField()
@@ -110,6 +183,7 @@ class Player(BaseModel):
fangr_id = CharField(null=True)
description = CharField()
quantity = IntegerField(default=999)
+ mlbplayer = ForeignKeyField(MlbPlayer, null=True)
def __str__(self):
return f'{self.cardset} {self.p_name} ({self.rarity.name})'
@@ -321,11 +395,35 @@ class Roster(BaseModel):
# this_roster.card26]
+class Result(BaseModel):
+ away_team = ForeignKeyField(Team)
+ home_team = ForeignKeyField(Team)
+ away_score = IntegerField()
+ home_score = IntegerField()
+ away_team_value = IntegerField(null=True)
+ home_team_value = IntegerField(null=True)
+ away_team_ranking = IntegerField(null=True)
+ home_team_ranking = IntegerField(null=True)
+ scorecard = CharField()
+ week = IntegerField()
+ season = IntegerField()
+ ranked = BooleanField()
+ short_game = BooleanField()
+ game_type = CharField(null=True)
+
+ @staticmethod
+ def select_season(season=None):
+ if not season:
+ season = Current.get().season
+ return Result.select().where(Result.season == season)
+
+
class BattingStat(BaseModel):
card = ForeignKeyField(Card)
team = ForeignKeyField(Team)
roster_num = IntegerField()
vs_team = ForeignKeyField(Team)
+ result = ForeignKeyField(Result, null=True)
pos = CharField()
pa = IntegerField()
ab = IntegerField()
@@ -364,6 +462,7 @@ class PitchingStat(BaseModel):
team = ForeignKeyField(Team)
roster_num = IntegerField()
vs_team = ForeignKeyField(Team)
+ result = ForeignKeyField(Result, null=True)
ip = FloatField()
hit = IntegerField()
run = IntegerField()
@@ -388,29 +487,6 @@ class PitchingStat(BaseModel):
game_id = IntegerField()
-class Result(BaseModel):
- away_team = ForeignKeyField(Team)
- home_team = ForeignKeyField(Team)
- away_score = IntegerField()
- home_score = IntegerField()
- away_team_value = IntegerField(null=True)
- home_team_value = IntegerField(null=True)
- away_team_ranking = IntegerField(null=True)
- home_team_ranking = IntegerField(null=True)
- scorecard = CharField()
- week = IntegerField()
- season = IntegerField()
- ranked = BooleanField()
- short_game = BooleanField()
- game_type = CharField(null=True)
-
- @staticmethod
- def select_season(season=None):
- if not season:
- season = Current.get().season
- return Result.select().where(Result.season == season)
-
-
class Award(BaseModel):
name = CharField()
season = IntegerField()
@@ -484,6 +560,7 @@ db.create_tables([
class BattingCard(BaseModel):
player = ForeignKeyField(Player)
+ variant = IntegerField()
steal_low = IntegerField()
steal_high = IntegerField()
steal_auto = BooleanField()
@@ -495,9 +572,16 @@ class BattingCard(BaseModel):
hand = CharField(default='R')
+bc_index = ModelIndex(BattingCard, (BattingCard.player, BattingCard.variant), unique=True)
+BattingCard.add_index(bc_index)
+
+
class BattingCardRatings(BaseModel):
battingcard = ForeignKeyField(BattingCard)
- vs_hand = FloatField()
+ vs_hand = CharField(default='R')
+ pull_rate = FloatField()
+ center_rate = FloatField()
+ slap_rate = FloatField()
homerun = FloatField()
bp_homerun = FloatField()
triple = FloatField()
@@ -525,20 +609,33 @@ class BattingCardRatings(BaseModel):
slg = FloatField(null=True)
+bcr_index = ModelIndex(
+ BattingCardRatings, (BattingCardRatings.battingcard, BattingCardRatings.vs_hand), unique=True
+)
+BattingCardRatings.add_index(bcr_index)
+
+
class PitchingCard(BaseModel):
player = ForeignKeyField(Player)
+ variant = IntegerField()
balk = IntegerField()
- wild_pitch = IntegerField(null=True)
- hold = CharField()
+ wild_pitch = IntegerField()
+ hold = IntegerField()
starter_rating = IntegerField()
relief_rating = IntegerField()
closer_rating = IntegerField(null=True)
batting = CharField(null=True)
+ offense_col = IntegerField()
+ hand = CharField(default='R')
+
+
+pc_index = ModelIndex(PitchingCard, (PitchingCard.player, PitchingCard.variant), unique=True)
+PitchingCard.add_index(pc_index)
class PitchingCardRatings(BaseModel):
pitchingcard = ForeignKeyField(PitchingCard)
- vs_hand = CharField()
+ vs_hand = CharField(default='R')
homerun = FloatField()
bp_homerun = FloatField()
triple = FloatField()
@@ -552,8 +649,9 @@ class PitchingCardRatings(BaseModel):
hbp = FloatField()
walk = FloatField()
strikeout = FloatField()
- fo_slap = FloatField()
- fo_center = FloatField()
+ flyout_lf_b = FloatField()
+ flyout_cf_b = FloatField()
+ flyout_rf_b = FloatField()
groundout_a = FloatField()
groundout_b = FloatField()
xcheck_p = FloatField()
@@ -570,10 +668,15 @@ class PitchingCardRatings(BaseModel):
slg = FloatField(null=True)
+pcr_index = ModelIndex(
+ PitchingCardRatings, (PitchingCardRatings.pitchingcard, PitchingCardRatings.vs_hand), unique=True
+)
+PitchingCardRatings.add_index(pcr_index)
+
+
class CardPosition(BaseModel):
player = ForeignKeyField(Player)
- batting = ForeignKeyField(BattingCard, null=True)
- pitching = ForeignKeyField(PitchingCard, null=True)
+ variant = IntegerField()
position = CharField()
innings = IntegerField()
range = IntegerField()
@@ -583,182 +686,188 @@ class CardPosition(BaseModel):
overthrow = IntegerField(null=True)
+pos_index = ModelIndex(
+ CardPosition, (CardPosition.player, CardPosition.variant, CardPosition.position), unique=True
+)
+CardPosition.add_index(pos_index)
+
+
db.create_tables([BattingCard, BattingCardRatings, PitchingCard, PitchingCardRatings, CardPosition])
db.close()
-scout_db = SqliteDatabase(
- 'storage/card_creation.db',
- pragmas={
- 'journal_mode': 'wal',
- 'cache_size': -1 * 64000,
- 'synchronous': 0
- }
-)
-
-
-class BaseModelScout(Model):
- class Meta:
- database = scout_db
-
-
-class ScoutCardset(BaseModelScout):
- set_title = CharField()
- set_subtitle = CharField(null=True)
-
-
-class ScoutPlayer(BaseModelScout):
- sba_id = IntegerField(primary_key=True)
- name = CharField()
- fg_id = IntegerField()
- br_id = CharField()
- offense_col = IntegerField()
- hand = CharField(default='R')
-
-
-scout_db.create_tables([ScoutCardset, ScoutPlayer])
-
-
-class BatterRatings(BaseModelScout):
- id = CharField(unique=True, primary_key=True)
- player = ForeignKeyField(ScoutPlayer)
- cardset = ForeignKeyField(ScoutCardset)
- vs_hand = FloatField()
- is_prep = BooleanField()
- homerun = FloatField()
- bp_homerun = FloatField()
- triple = FloatField()
- double_three = FloatField()
- double_two = FloatField()
- double_pull = FloatField()
- single_two = FloatField()
- single_one = FloatField()
- single_center = FloatField()
- bp_single = FloatField()
- hbp = FloatField()
- walk = FloatField()
- strikeout = FloatField()
- lineout = FloatField()
- popout = FloatField()
- flyout_a = FloatField()
- flyout_bq = FloatField()
- flyout_lf_b = FloatField()
- flyout_rf_b = FloatField()
- groundout_a = FloatField()
- groundout_b = FloatField()
- groundout_c = FloatField()
- avg = FloatField(null=True)
- obp = FloatField(null=True)
- slg = FloatField(null=True)
-
-
-class PitcherRatings(BaseModelScout):
- id = CharField(unique=True, primary_key=True)
- player = ForeignKeyField(ScoutPlayer)
- cardset = ForeignKeyField(ScoutCardset)
- vs_hand = CharField()
- is_prep = BooleanField()
- homerun = FloatField()
- bp_homerun = FloatField()
- triple = FloatField()
- double_three = FloatField()
- double_two = FloatField()
- double_cf = FloatField()
- single_two = FloatField()
- single_one = FloatField()
- single_center = FloatField()
- bp_single = FloatField()
- hbp = FloatField()
- walk = FloatField()
- strikeout = FloatField()
- fo_slap = FloatField()
- fo_center = FloatField()
- groundout_a = FloatField()
- groundout_b = FloatField()
- xcheck_p = FloatField()
- xcheck_c = FloatField()
- xcheck_1b = FloatField()
- xcheck_2b = FloatField()
- xcheck_3b = FloatField()
- xcheck_ss = FloatField()
- xcheck_lf = FloatField()
- xcheck_cf = FloatField()
- xcheck_rf = FloatField()
- avg = FloatField(null=True)
- obp = FloatField(null=True)
- slg = FloatField(null=True)
-
-
-# scout_db.create_tables([BatterRatings, PitcherRatings])
-
-
-class CardColumns(BaseModelScout):
- id = CharField(unique=True, primary_key=True)
- player = ForeignKeyField(ScoutPlayer)
- hand = CharField()
- b_ratings = ForeignKeyField(BatterRatings, null=True)
- p_ratings = ForeignKeyField(PitcherRatings, null=True)
- one_dice = CharField()
- one_results = CharField()
- one_splits = CharField()
- two_dice = CharField()
- two_results = CharField()
- two_splits = CharField()
- three_dice = CharField()
- three_results = CharField()
- three_splits = CharField()
-
-
-class Position(BaseModelScout):
- player = ForeignKeyField(ScoutPlayer)
- cardset = ForeignKeyField(ScoutCardset)
- position = CharField()
- innings = IntegerField()
- range = IntegerField()
- error = IntegerField()
- arm = CharField(null=True)
- pb = IntegerField(null=True)
- overthrow = IntegerField(null=True)
-
-
-class BatterData(BaseModelScout):
- player = ForeignKeyField(ScoutPlayer)
- cardset = ForeignKeyField(ScoutCardset)
- stealing = CharField()
- st_low = IntegerField()
- st_high = IntegerField()
- st_auto = BooleanField()
- st_jump = FloatField()
- bunting = CharField(null=True)
- hit_and_run = CharField(null=True)
- running = CharField()
-
-
-class PitcherData(BaseModelScout):
- player = ForeignKeyField(ScoutPlayer)
- cardset = ForeignKeyField(ScoutCardset)
- balk = IntegerField(null=True)
- wild_pitch = IntegerField(null=True)
- hold = CharField()
- starter_rating = IntegerField()
- relief_rating = IntegerField()
- closer_rating = IntegerField(null=True)
- batting = CharField(null=True)
-
-
-scout_db.create_tables([CardColumns, Position, BatterData, PitcherData])
-
-
-class CardOutput(BaseModelScout):
- name = CharField()
- hand = CharField()
- positions = CharField()
- stealing = CharField()
- bunting = CharField()
- hitandrun = CharField()
- running = CharField()
-
-
-scout_db.close()
+# scout_db = SqliteDatabase(
+# 'storage/card_creation.db',
+# pragmas={
+# 'journal_mode': 'wal',
+# 'cache_size': -1 * 64000,
+# 'synchronous': 0
+# }
+# )
+#
+#
+# class BaseModelScout(Model):
+# class Meta:
+# database = scout_db
+#
+#
+# class ScoutCardset(BaseModelScout):
+# set_title = CharField()
+# set_subtitle = CharField(null=True)
+#
+#
+# class ScoutPlayer(BaseModelScout):
+# sba_id = IntegerField(primary_key=True)
+# name = CharField()
+# fg_id = IntegerField()
+# br_id = CharField()
+# offense_col = IntegerField()
+# hand = CharField(default='R')
+#
+#
+# scout_db.create_tables([ScoutCardset, ScoutPlayer])
+#
+#
+# class BatterRatings(BaseModelScout):
+# id = CharField(unique=True, primary_key=True)
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# vs_hand = FloatField()
+# is_prep = BooleanField()
+# homerun = FloatField()
+# bp_homerun = FloatField()
+# triple = FloatField()
+# double_three = FloatField()
+# double_two = FloatField()
+# double_pull = FloatField()
+# single_two = FloatField()
+# single_one = FloatField()
+# single_center = FloatField()
+# bp_single = FloatField()
+# hbp = FloatField()
+# walk = FloatField()
+# strikeout = FloatField()
+# lineout = FloatField()
+# popout = FloatField()
+# flyout_a = FloatField()
+# flyout_bq = FloatField()
+# flyout_lf_b = FloatField()
+# flyout_rf_b = FloatField()
+# groundout_a = FloatField()
+# groundout_b = FloatField()
+# groundout_c = FloatField()
+# avg = FloatField(null=True)
+# obp = FloatField(null=True)
+# slg = FloatField(null=True)
+#
+#
+# class PitcherRatings(BaseModelScout):
+# id = CharField(unique=True, primary_key=True)
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# vs_hand = CharField()
+# is_prep = BooleanField()
+# homerun = FloatField()
+# bp_homerun = FloatField()
+# triple = FloatField()
+# double_three = FloatField()
+# double_two = FloatField()
+# double_cf = FloatField()
+# single_two = FloatField()
+# single_one = FloatField()
+# single_center = FloatField()
+# bp_single = FloatField()
+# hbp = FloatField()
+# walk = FloatField()
+# strikeout = FloatField()
+# fo_slap = FloatField()
+# fo_center = FloatField()
+# groundout_a = FloatField()
+# groundout_b = FloatField()
+# xcheck_p = FloatField()
+# xcheck_c = FloatField()
+# xcheck_1b = FloatField()
+# xcheck_2b = FloatField()
+# xcheck_3b = FloatField()
+# xcheck_ss = FloatField()
+# xcheck_lf = FloatField()
+# xcheck_cf = FloatField()
+# xcheck_rf = FloatField()
+# avg = FloatField(null=True)
+# obp = FloatField(null=True)
+# slg = FloatField(null=True)
+#
+#
+# # scout_db.create_tables([BatterRatings, PitcherRatings])
+#
+#
+# class CardColumns(BaseModelScout):
+# id = CharField(unique=True, primary_key=True)
+# player = ForeignKeyField(ScoutPlayer)
+# hand = CharField()
+# b_ratings = ForeignKeyField(BatterRatings, null=True)
+# p_ratings = ForeignKeyField(PitcherRatings, null=True)
+# one_dice = CharField()
+# one_results = CharField()
+# one_splits = CharField()
+# two_dice = CharField()
+# two_results = CharField()
+# two_splits = CharField()
+# three_dice = CharField()
+# three_results = CharField()
+# three_splits = CharField()
+#
+#
+# class Position(BaseModelScout):
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# position = CharField()
+# innings = IntegerField()
+# range = IntegerField()
+# error = IntegerField()
+# arm = CharField(null=True)
+# pb = IntegerField(null=True)
+# overthrow = IntegerField(null=True)
+#
+#
+# class BatterData(BaseModelScout):
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# stealing = CharField()
+# st_low = IntegerField()
+# st_high = IntegerField()
+# st_auto = BooleanField()
+# st_jump = FloatField()
+# bunting = CharField(null=True)
+# hit_and_run = CharField(null=True)
+# running = CharField()
+#
+#
+# class PitcherData(BaseModelScout):
+# player = ForeignKeyField(ScoutPlayer)
+# cardset = ForeignKeyField(ScoutCardset)
+# balk = IntegerField(null=True)
+# wild_pitch = IntegerField(null=True)
+# hold = CharField()
+# starter_rating = IntegerField()
+# relief_rating = IntegerField()
+# closer_rating = IntegerField(null=True)
+# batting = CharField(null=True)
+#
+#
+# scout_db.create_tables([CardColumns, Position, BatterData, PitcherData])
+#
+#
+# class CardOutput(BaseModelScout):
+# name = CharField()
+# hand = CharField()
+# positions = CharField()
+# stealing = CharField()
+# bunting = CharField()
+# hitandrun = CharField()
+# running = CharField()
+#
+#
+# scout_db.close()
diff --git a/db_migrations.py b/db_migrations.py
index 7559e0a..07958b1 100644
--- a/db_migrations.py
+++ b/db_migrations.py
@@ -11,23 +11,28 @@ migrator = SqliteMigrator(db_engine.db)
# pitcher_injury = IntegerField(null=True)
-# pos_1 = CharField(default='None')
+# offense_col = IntegerField(null=True)
# pos_2 = CharField(null=True)
# last_game = CharField(null=True)
# game_type = CharField(null=True)
-# pack_type = ForeignKeyField(PackType, default=1, to_field='id', field_type=int)
+mlb_player = ForeignKeyField(db_engine.MlbPlayer, field=db_engine.MlbPlayer.id, null=True)
+result = ForeignKeyField(db_engine.Result, field=db_engine.Result.id, null=True)
# active_theme = ForeignKeyField(PackTheme, to_field='id', field_type=int, null=True)
# active_theme = ForeignKeyField(db_engine.PackTheme, field=db_engine.PackTheme.id, null=True) # for careers
# game_type = CharField(null=True)
# pack_team = ForeignKeyField(db_engine.Team, field=db_engine.Team.id, null=True)
-pack_cardset = ForeignKeyField(db_engine.Cardset, field=db_engine.Cardset.id, null=True)
+# pack_cardset = ForeignKeyField(db_engine.Cardset, field=db_engine.Cardset.id, null=True)
+pull_rate = FloatField(default=0.333)
migrate(
# migrator.add_column('current', 'active_theme_id', active_theme),
# migrator.add_column('pack', 'pack_team_id', pack_team),
- migrator.add_column('pack', 'pack_cardset_id', pack_cardset),
+ # migrator.add_column('player', 'mlbplayer_id', mlb_player),
+ migrator.add_column('battingstat', 'result_id', result),
+ migrator.add_column('pitchingstat', 'result_id', result),
+ # migrator.add_column('battingcardratings', 'pull_rate', pull_rate),
# migrator.rename_column('cardset', 'available', 'for_purchase')
- # migrator.add_column('player', 'pos_1', pos_1),
+ # migrator.add_column('player', 'offense_col', offense_col),
# migrator.add_column('comment_tbl', 'comment', comment_field),
# migrator.rename_column('story', 'pub_date', 'publish_date'),
# migrator.drop_column('story', 'some_old_field'),
diff --git a/requirements.txt b/requirements.txt
index ecccb5d..9d19235 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,8 @@ peewee
python-multipart
pandas
pygsheets
+pybaseball
+python-multipart
+requests
+html2image
+jinja2
diff --git a/venv/share/man/man1/ttx.1 b/venv/share/man/man1/ttx.1
new file mode 100644
index 0000000..bba23b5
--- /dev/null
+++ b/venv/share/man/man1/ttx.1
@@ -0,0 +1,225 @@
+.Dd May 18, 2004
+.\" ttx is not specific to any OS, but contrary to what groff_mdoc(7)
+.\" seems to imply, entirely omitting the .Os macro causes 'BSD' to
+.\" be used, so I give a zero-width space as its argument.
+.Os \&
+.\" The "FontTools Manual" argument apparently has no effect in
+.\" groff 1.18.1. I think it is a bug in the -mdoc groff package.
+.Dt TTX 1 "FontTools Manual"
+.Sh NAME
+.Nm ttx
+.Nd tool for manipulating TrueType and OpenType fonts
+.Sh SYNOPSIS
+.Nm
+.Bk
+.Op Ar option ...
+.Ek
+.Bk
+.Ar file ...
+.Ek
+.Sh DESCRIPTION
+.Nm
+is a tool for manipulating TrueType and OpenType fonts. It can convert
+TrueType and OpenType fonts to and from an
+.Tn XML Ns -based format called
+.Tn TTX .
+.Tn TTX
+files have a
+.Ql .ttx
+extension.
+.Pp
+For each
+.Ar file
+argument it is given,
+.Nm
+detects whether it is a
+.Ql .ttf ,
+.Ql .otf
+or
+.Ql .ttx
+file and acts accordingly: if it is a
+.Ql .ttf
+or
+.Ql .otf
+file, it generates a
+.Ql .ttx
+file; if it is a
+.Ql .ttx
+file, it generates a
+.Ql .ttf
+or
+.Ql .otf
+file.
+.Pp
+By default, every output file is created in the same directory as the
+corresponding input file and with the same name except for the
+extension, which is substituted appropriately.
+.Nm
+never overwrites existing files; if necessary, it appends a suffix to
+the output file name before the extension, as in
+.Pa Arial#1.ttf .
+.Ss "General options"
+.Bl -tag -width ".Fl t Ar table"
+.It Fl h
+Display usage information.
+.It Fl d Ar dir
+Write the output files to directory
+.Ar dir
+instead of writing every output file to the same directory as the
+corresponding input file.
+.It Fl o Ar file
+Write the output to
+.Ar file
+instead of writing it to the same directory as the
+corresponding input file.
+.It Fl v
+Be verbose. Write more messages to the standard output describing what
+is being done.
+.It Fl a
+Allow virtual glyphs ID's on compile or decompile.
+.El
+.Ss "Dump options"
+The following options control the process of dumping font files
+(TrueType or OpenType) to
+.Tn TTX
+files.
+.Bl -tag -width ".Fl t Ar table"
+.It Fl l
+List table information. Instead of dumping the font to a
+.Tn TTX
+file, display minimal information about each table.
+.It Fl t Ar table
+Dump table
+.Ar table .
+This option may be given multiple times to dump several tables at
+once. When not specified, all tables are dumped.
+.It Fl x Ar table
+Exclude table
+.Ar table
+from the list of tables to dump. This option may be given multiple
+times to exclude several tables from the dump. The
+.Fl t
+and
+.Fl x
+options are mutually exclusive.
+.It Fl s
+Split tables. Dump each table to a separate
+.Tn TTX
+file and write (under the name that would have been used for the output
+file if the
+.Fl s
+option had not been given) one small
+.Tn TTX
+file containing references to the individual table dump files. This
+file can be used as input to
+.Nm
+as long as the referenced files can be found in the same directory.
+.It Fl i
+.\" XXX: I suppose OpenType programs (exist and) are also affected.
+Don't disassemble TrueType instructions. When this option is specified,
+all TrueType programs (glyph programs, the font program and the
+pre-program) are written to the
+.Tn TTX
+file as hexadecimal data instead of
+assembly. This saves some time and results in smaller
+.Tn TTX
+files.
+.It Fl y Ar n
+When decompiling a TrueType Collection (TTC) file,
+decompile font number
+.Ar n ,
+starting from 0.
+.El
+.Ss "Compilation options"
+The following options control the process of compiling
+.Tn TTX
+files into font files (TrueType or OpenType):
+.Bl -tag -width ".Fl t Ar table"
+.It Fl m Ar fontfile
+Merge the input
+.Tn TTX
+file
+.Ar file
+with
+.Ar fontfile .
+No more than one
+.Ar file
+argument can be specified when this option is used.
+.It Fl b
+Don't recalculate glyph bounding boxes. Use the values in the
+.Tn TTX
+file as is.
+.El
+.Sh "THE TTX FILE FORMAT"
+You can find some information about the
+.Tn TTX
+file format in
+.Pa documentation.html .
+In particular, you will find in that file the list of tables understood by
+.Nm
+and the relations between TrueType GlyphIDs and the glyph names used in
+.Tn TTX
+files.
+.Sh EXAMPLES
+In the following examples, all files are read from and written to the
+current directory. Additionally, the name given for the output file
+assumes in every case that it did not exist before
+.Nm
+was invoked.
+.Pp
+Dump the TrueType font contained in
+.Pa FreeSans.ttf
+to
+.Pa FreeSans.ttx :
+.Pp
+.Dl ttx FreeSans.ttf
+.Pp
+Compile
+.Pa MyFont.ttx
+into a TrueType or OpenType font file:
+.Pp
+.Dl ttx MyFont.ttx
+.Pp
+List the tables in
+.Pa FreeSans.ttf
+along with some information:
+.Pp
+.Dl ttx -l FreeSans.ttf
+.Pp
+Dump the
+.Sq cmap
+table from
+.Pa FreeSans.ttf
+to
+.Pa FreeSans.ttx :
+.Pp
+.Dl ttx -t cmap FreeSans.ttf
+.Sh NOTES
+On MS\-Windows and MacOS,
+.Nm
+is available as a graphical application to which files can be dropped.
+.Sh SEE ALSO
+.Pa documentation.html
+.Pp
+.Xr fontforge 1 ,
+.Xr ftinfo 1 ,
+.Xr gfontview 1 ,
+.Xr xmbdfed 1 ,
+.Xr Font::TTF 3pm
+.Sh AUTHORS
+.Nm
+was written by
+.An -nosplit
+.An "Just van Rossum" Aq just@letterror.com .
+.Pp
+This manual page was written by
+.An "Florent Rougon" Aq f.rougon@free.fr
+for the Debian GNU/Linux system based on the existing FontTools
+documentation. It may be freely used, modified and distributed without
+restrictions.
+.\" For Emacs:
+.\" Local Variables:
+.\" fill-column: 72
+.\" sentence-end: "[.?!][]\"')}]*\\($\\| $\\| \\| \\)[ \n]*"
+.\" sentence-end-double-space: t
+.\" End:
\ No newline at end of file