diff --git a/Pipfile.lock b/Pipfile.lock index 7e0a6d4..1e23fdd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,17 +18,17 @@ "default": { "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "version": "==4.0.0" }, "idna": { "hashes": [ @@ -39,11 +39,11 @@ }, "praw": { "hashes": [ - "sha256:6b93ad1e53385c68753203ec87f4d0053b2425b09fc8813847800a542efdfe6c", - "sha256:fb55e46203a771342da7cbe144fbcd8c61d825719ce1025bdd72112194a0228f" + "sha256:068a01c19834e1f748a8c220c6aa62ae9f0f8e211504ab8ef19883d09bed3430", + "sha256:87166a77ec31a1d9686ccdac97b5b72ba277ce436976eb3baf467c593f15bb26" ], "index": "pypi", - "version": "==7.1.0" + "version": "==7.1.4" }, "prawcore": { "hashes": [ @@ -54,10 +54,10 @@ }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "version": "==2.24.0" + "version": "==2.25.1" }, "six": { "hashes": [ @@ -68,11 +68,11 @@ }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], "index": "pypi", - "version": "==0.10.1" + "version": "==0.10.2" }, "update-checker": { "hashes": [ @@ -83,10 +83,10 @@ }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "version": "==1.25.10" + "version": "==1.26.3" }, "websocket-client": { "hashes": [ @@ -99,51 +99,54 @@ "develop": { "astroid": { "hashes": [ - "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", - "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" + "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc", + "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d" ], - "version": "==2.4.2" + "version": "==2.5" }, "colorama": { "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], "markers": "sys_platform == 'win32'", - "version": "==0.4.3" + "version": "==0.4.4" }, "isort": { "hashes": [ - "sha256:60a1b97e33f61243d12647aaaa3e6cc6778f5eb9f42997650f1cc975b6008750", - "sha256:d488ba1c5a2db721669cc180180d5acf84ebdc5af7827f7aaeaa75f73cf0e2b8" + "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", + "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" ], - "version": "==5.4.2" + "version": "==5.7.0" }, "lazy-object-proxy": { "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39", + "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694", + "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9", + "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84", + "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa", + "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128", + "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871", + "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0", + "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4", + "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302", + "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458", + "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c", + "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7", + "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb", + "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163", + "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847", + "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108", + "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef", + "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f", + "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315", + "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068", + "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166", + "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a", + "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d" ], - "version": "==1.4.3" + "version": "==1.5.2" }, "mccabe": { "hashes": [ @@ -160,47 +163,49 @@ "index": "pypi", "version": "==2.6.0" }, - "six": { - "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "version": "==1.15.0" - }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], "index": "pypi", - "version": "==0.10.1" + "version": "==0.10.2" }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.1" + "version": "==1.4.2" }, "wrapt": { "hashes": [ diff --git a/PointsBot.py b/PointsBot.py index f4ccf9e..92cb823 100644 --- a/PointsBot.py +++ b/PointsBot.py @@ -1,4 +1,7 @@ """Entry point used for either runnning or freezing the bot.""" import pointsbot -pointsbot.run() +try: + pointsbot.run() +except KeyboardInterrupt as e: + print('\nShutting down...\n') diff --git a/pointsbot/bot.py b/pointsbot/bot.py index 38ab944..7146928 100644 --- a/pointsbot/bot.py +++ b/pointsbot/bot.py @@ -54,7 +54,7 @@ def run(): subreddit = reddit.subreddit(cfg.subreddit) logging.info('Watching subreddit %s', subreddit.title) is_mod = subreddit.moderator(redditor=reddit.user.me()) - logging.info(f'Is {"" if is_mod else "NOT "} moderator for subreddit') + logging.info(f'Is {"" if is_mod else "NOT "}moderator for subreddit') monitor_comments(reddit, subreddit, db, levels, cfg) @@ -306,7 +306,7 @@ def is_valid_tag(solved_comment, valid_tags): # def find_solver(solved_comment): -def find_solver_and_comment(solved_comment) +def find_solver_and_comment(solved_comment): """Determine the redditor responsible for solving the question.""" # TODO plz make this better someday # return solved_comment.parent().author diff --git a/pointsbot/database.py b/pointsbot/database.py index c07b9d1..92870db 100644 --- a/pointsbot/database.py +++ b/pointsbot/database.py @@ -1,15 +1,17 @@ import datetime import functools +import logging import os.path +import re import sqlite3 as sqlite -from collections import namedtuple ### Decorators ### def transaction(func): """Use this decorator on any methods that needs to query the database to - ensure that connections are properly opened and closed. + ensure that connections are properly opened and closed, without being + left open unnecessarily. """ @functools.wraps(func) def newfunc(self, *args, **kwargs): @@ -20,7 +22,16 @@ def transaction(func): self.cursor = self.conn.cursor() created_conn = True - ret = func(self, *args, **kwargs) + try: + return_value = func(self, *args, **kwargs) + except Exception as e: + if self.conn.in_transaction: + self.conn.rollback() + if created_conn: + self.cursor.close() + self.conn.close() + self.cursor = self.conn = None + raise e if self.conn.in_transaction: self.conn.commit() @@ -30,75 +41,114 @@ def transaction(func): self.conn.close() self.cursor = self.conn = None - return ret + return return_value + return newfunc ### Classes ### -DatabaseVersion = namedtuple('DatabaseVersion', 'major minor patch pre_release_name pre_release_number') +class DatabaseVersion: + + PRE_RELEASE_NAME_ORDER_NUMBER = { + None: None, + 'alpha': 0, + 'beta': 1, + 'rc': 2 + } + + def __init__(self, major, minor, patch, pre_release_name=None, pre_release_number=None): + self.major = major + self.minor = minor + self.patch = patch + self.pre_release_name = pre_release_name + self.pre_release_number = pre_release_number + + def __lt__(self, other): + self_tuple = (self.major, self.minor, self.patch, self.PRE_RELEASE_NAME_ORDER_NUMBER[self.pre_release_name], self.pre_release_number) + other_tuple = (other.major, other.minor, other.patch, self.PRE_RELEASE_NAME_ORDER_NUMBER[other.pre_release_name], other.pre_release_number) + return self_tuple < other_tuple + + def __str__(self): + version_string = f'{self.major}.{self.minor}.{self.patch}' + if self.pre_release_name is not None: + version_string += f'-{self.pre_release_name}' + if self.pre_release_number is not None: + version_string += f'.{self.pre_release_number}' + return version_string + + @classmethod + def from_string(cls, version_string): + match = re.match(r'(\d+).(\d+).(\d+)(?:-([:alpha:]+)(?:.(\d+))?)?', version_string) + if not match: + return None + groups = match.groups() + return cls(int(groups[0]), int(groups[1]), int(groups[2]), groups[3], int(groups[4])) class Database: - VERSION = DatabaseVersion(0, 2, 0, None, None) + # LATEST_VERSION = DatabaseVersion(0, 2, 0, None, None) + LATEST_VERSION = DatabaseVersion(0, 2, 0) - SCHEMA = ''' - --------------------------- - -- Schema version: 0.1.0 -- - --------------------------- - - CREATE TABLE IF NOT EXISTS redditor_points ( - id TEXT UNIQUE NOT NULL, - name TEXT UNIQUE NOT NULL, - points INTEGER DEFAULT 0 - ); - - --------------------------- - -- Schema version: 0.2.0 -- - --------------------------- - - -- Tracking bot/db version for potential future use in migrations et al. - CREATE TABLE IF NOT EXISTS bot_version ( - major INTEGER NOT NULL, - minor INTEGER NOT NULL, - patch INTEGER NOT NULL, - pre_release_name TEXT, - pre_release_number INTEGER - ); - INSERT OR IGNORE INTO bot_version (major, minor, patch) VALUES (0, 2, 0); - - ALTER TABLE redditor_points RENAME TO redditor; - -- TODO rename "id" columns to "reddit_id" for consistency/clarity? - -- ALTER TABLE redditor RENAME COLUMN id TO reddit_id; - - CREATE TABLE IF NOT EXISTS submission ( - id TEXT UNIQUE NOT NULL, - author_id TEXT UNIQUE NOT NULL - ); - - CREATE TABLE IF NOT EXISTS comment ( - id TEXT UNIQUE NOT NULL, - author_id TEXT NOT NULL, - author_rowid INTEGER, -- May be NULL **for now** - created_at_datetime TEXT NOT NULL, - FOREIGN KEY (author_rowid) REFERENCES redditor (rowid) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS solution ( - submission_rowid INTEGER NOT NULL, - author_rowid INTEGER NOT NULL, - comment_rowid INTEGER NOT NULL, - chosen_by_comment_rowid INTEGER NOT NULL, - removed_by_comment_rowid INTEGER, - FOREIGN KEY (submission_rowid) REFERENCES submission (rowid) ON DELETE CASCADE, - FOREIGN KEY (author_rowid) REFERENCES redditor (rowid) ON DELETE CASCADE, - FOREIGN KEY (comment_rowid) REFERENCES comment (rowid) ON DELETE CASCADE, - FOREIGN KEY (chosen_by_comment_rowid) REFERENCES comment (rowid) ON DELETE SET NULL, - FOREIGN KEY (removed_by_comment_rowid) REFERENCES comment (rowid) ON DELETE SET NULL, - PRIMARY KEY (submission_rowid, author_rowid) ON DELETE CASCADE - ); - ''' + SCHEMA_VERSION_STATEMENTS = { + DatabaseVersion(0, 1, 0): [ + ''' + CREATE TABLE IF NOT EXISTS redditor_points ( + id TEXT UNIQUE NOT NULL, + name TEXT UNIQUE NOT NULL, + points INTEGER DEFAULT 0 + ) + ''' + ], + DatabaseVersion(0, 2, 0): [ + ''' + CREATE TABLE IF NOT EXISTS bot_version ( + major INTEGER NOT NULL, + minor INTEGER NOT NULL, + patch INTEGER NOT NULL, + pre_release_name TEXT, + pre_release_number INTEGER + ) + ''', + ''' + INSERT OR IGNORE INTO bot_version (major, minor, patch) VALUES (0, 2, 0) + ''', + ''' + ALTER TABLE redditor_points RENAME TO redditor + ''', + ''' + CREATE TABLE IF NOT EXISTS submission ( + id TEXT UNIQUE NOT NULL, + author_id TEXT UNIQUE NOT NULL + ) + ''', + ''' + CREATE TABLE IF NOT EXISTS comment ( + id TEXT UNIQUE NOT NULL, + author_id TEXT NOT NULL, + author_rowid INTEGER, -- May be NULL **for now** + created_at_datetime TEXT NOT NULL, + FOREIGN KEY (author_rowid) REFERENCES redditor (rowid) ON DELETE CASCADE + ) + ''', + ''' + CREATE TABLE IF NOT EXISTS solution ( + submission_rowid INTEGER NOT NULL, + author_rowid INTEGER NOT NULL, + comment_rowid INTEGER NOT NULL, + chosen_by_comment_rowid INTEGER NOT NULL, + removed_by_comment_rowid INTEGER, + FOREIGN KEY (submission_rowid) REFERENCES submission (rowid) ON DELETE CASCADE, + FOREIGN KEY (author_rowid) REFERENCES redditor (rowid) ON DELETE CASCADE, + FOREIGN KEY (comment_rowid) REFERENCES comment (rowid) ON DELETE CASCADE, + FOREIGN KEY (chosen_by_comment_rowid) REFERENCES comment (rowid) ON DELETE SET NULL, + FOREIGN KEY (removed_by_comment_rowid) REFERENCES comment (rowid) ON DELETE SET NULL, + PRIMARY KEY (submission_rowid, author_rowid) + ) + ''' + ] + } def __init__(self, dbpath): self.path = dbpath @@ -106,28 +156,41 @@ class Database: self.cursor = None if not os.path.exists(self.path): - self._create() + logging.info('No database found; creating...') + self._run_migrations() + logging.info('Successfully created database') else: - self._migrate_if_necessary() + current_version = self._get_current_version() + if current_version != self.LATEST_VERSION: + logging.info('Newer database version exists; migrating...') + self._run_migrations(current_version) + logging.info('Successfully completed all migrations') @transaction - def _create(self): - self.cursor.execute(self.SCHEMA) + def _run_migrations(self, current_version=None): + if not current_version: + current_version = DatabaseVersion(0, 0, 0) + logging.info(f'Current database version: {current_version}') + versions = sorted(v for v in self.SCHEMA_VERSION_STATEMENTS if current_version < v) + for v in versions: + logging.info(f'Beginning migration to version: {v}...') + for sql_stmt in self.SCHEMA_VERSION_STATEMENTS[v]: + self.cursor.execute(sql_stmt) + logging.info(f'Successfully completed migration') @transaction - def _migrate_if_necessary(self): + def _get_current_version(self): self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'bot_version'") has_version_table = (self.cursor.rowcount == 1) - has_outdated_version = False - if has_version_table: + if not has_version_table: + current_version = DatabaseVersion(0, 1, 0) + else: self.cursor.execute('SELECT major, minor, patch, pre_release_name, pre_release_number FROM bot_version') row = self.cursor.fetchone() pre_release_number = int(row['pre_release_number']) if row['pre_release_number'] else None current_version = DatabaseVersion(int(row['major']), int(row['minor']), int(row['patch']), row['pre_release_name'], pre_release_number) - has_outdated_version = (current_version == self.VERSION) - if not has_version_table or has_outdated_version: - self.cursor.execute(self.SCHEMA) + return current_version ### Public Methods ### @@ -151,18 +214,14 @@ class Database: return self.cursor.rowcount @transaction - # def has_already_solved_once(self, solver, submission): def has_already_solved_once(self, submission, solver): - # author_id = self._get_rowid_from_reddit_id('redditor', solver) select_stmt = ''' SELECT count(solution.rowid) AS num_solutions FROM solution JOIN submission ON (solution.submission_rowid = submission.rowid) JOIN redditor ON (solution.author_rowid = redditor.rowid) - -- JOIN comment ON (solution.comment_rowid = comment.rowid) WHERE submission.id = :submission_id AND redditor.id = :author_id - -- AND comment.author_id = :author_id ''' self.cursor.execute(select_stmt, {'submission_id': submission.id, 'author_id': solver.id}) row = self.cursor.fetchone() @@ -174,32 +233,24 @@ class Database: self._add_comment(chosen_by_comment, chooser) self._update_points(solver, 1) - # rowcount = self._add_solution(submission, solution_comment, chosen_by_comment) rowcount = self._add_solution(submission, solver, solution_comment, chosen_by_comment) if rowcount == 0: # Was not able to add solution, probably because user has already solved this submission self._update_points(solver, -1) # if rowcount > 0: - # rowcount = self._update_points(solver, 1) # # TODO update author_rowid for comment? return rowcount - # def remove_point_for_solution(self, submission, solver, solution_comment, remover, removed_by_comment): - # def remove_point_for_solution(self, submission, solver, remover, removed_by_comment): def soft_remove_point_for_solution(self, submission, solver, remover, removed_by_comment): - # submission = removed_by_comment.submission self._add_comment(removed_by_comment, remover) - # rowcount = self._soft_remove_solution(submission, solution_comment, removed_by_comment) rowcount = self._soft_remove_solution(submission, solver, removed_by_comment) if rowcount > 0: rowcount = self._update_points(solver, -1) - # TODO move "remove redditor" logic here since it doesn't need to be considered when adding points? return rowcount @transaction def add_back_point_for_solution(self, submission, solver): self._update_points(solver, 1) - # submission_rowid = self._get_submission_rowid(submission) submission_rowid = self._get_rowid_from_reddit_id('submission', submission) author_rowid = self._get_rowid_from_reddit_id('redditor', solver) params = {'submission_rowid': submission_rowid, 'author_rowid': author_rowid} @@ -266,12 +317,6 @@ class Database: self.cursor.execute(insert_stmt, params) return self.cursor.rowcount - # @transaction - # def _get_comment_rowid(self, comment): - # self.cursor.execute('SELECT rowid FROM comment WHERE id = :id', {'id': comment.id}) - # row = self.cursor.fetchone() - # return row['rowid'] if row else None - @transaction def _add_submission(self, submission): insert_stmt = ''' @@ -281,17 +326,8 @@ class Database: self.cursor.execute(insert_stmt, {'id': submission.id, 'author_id': submission.author.id}) return self.cursor.rowcount - # @transaction - # def _get_submission_rowid(self, submission): - # self.cursor.execute('SELECT rowid FROM submission WHERE id = :id', {'id': submission.id}) - # row = self.cursor.fetchone() - # return row['rowid'] if row else None - @transaction def _add_solution(self, submission, solver, comment, chosen_by_comment): - # submission_rowid = self._get_submission_rowid(submission) - # comment_rowid = self._get_comment_rowid(comment) - # chosen_by_comment_rowid = self._get_comment_rowid(chosen_by_comment) submission_rowid = self._get_rowid_from_reddit_id('submission', submission) author_rowid = self._get_rowid_from_reddit_id('redditor', solver) comment_rowid = self._get_rowid_from_reddit_id('comment', comment) @@ -310,18 +346,12 @@ class Database: return self.cursor.rowcount @transaction - # def _soft_remove_solution(self, submission, comment, removed_by_comment): def _soft_remove_solution(self, submission, solver, removed_by_comment): - # submission_rowid = self._get_submission_rowid(submission) - # comment_rowid = self._get_comment_rowid(comment) - # removed_by_comment_rowid = self._get_comment_rowid(removed_by_comment) submission_rowid = self._get_rowid_from_reddit_id('submission', submission) author_rowid = self._get_rowid_from_reddit_id('redditor', solver) - # comment_rowid = self._get_rowid_from_reddit_id('comment', comment) removed_by_comment_rowid = self._get_rowid_from_reddit_id('comment', removed_by_comment) params = { 'submission_rowid': submission_rowid, - # 'comment_rowid': comment_rowid, 'author_rowid': author_rowid, 'removed_by_comment_rowid': removed_by_comment_rowid, } @@ -333,12 +363,6 @@ class Database: self.cursor.execute(update_stmt, params) return self.cursor.rowcount - # @transaction - # def _get_redditor_rowid(self, redditor): - # self.cursor.execute('SELECT rowid FROM redditor WHERE id = :id', {'id': redditor.id}) - # row = self.cursor.fetchone() - # return row['rowid'] if row else None - @transaction def _update_points(self, redditor, points_modifier): """points_modifier is positive to add points, negative to subtract."""