Repository: noenfugler/LemmyModBot (language: python)

Issue:
Add 3 strikes report function.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 729cb89..82dfa7c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -75,12 +75,16 @@ jobs:
         uses: docker/setup-qemu-action@v2
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
+      - id: repo
+        uses: ASzc/change-string-case-action@v5
+        with:
+          string: ${{ github.repository }}
       - name: Docker meta
         id: meta
         uses: docker/metadata-action@v4
         with:
           images: |
-            ghcr.io/benmmclean/lemmymodbot
+            ghcr.io/${{ steps.repo.outputs.lowercase }}
           tags: |
             type=schedule
             type=ref,event=branch
@@ -103,5 +107,5 @@ jobs:
           push: true
           tags: |
             ${{ steps.meta.outputs.tags }}
-            ghcr.io/benmmclean/lemmymodbot:latest
-            ghcr.io/benmmclean/lemmymodbot:${{ needs.version.outputs.version }}
+            ghcr.io/${{ steps.repo.outputs.lowercase }}:latest
+            ghcr.io/${{ steps.repo.outputs.lowercase }}:${{ needs.version.outputs.version }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..e7e2981
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,26 @@
+name: Lint
+
+on:
+  push:
+    branches: [ "master", "develop" ]
+  pull_request:
+    branches: [ "master", "develop" ]
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.11'
+          cache: 'pip'
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements.txt
+      - name: Lint with Ruff
+        run: |
+          pip install ruff
+          ruff --output-format=github .
diff --git a/.gitignore b/.gitignore
index 8345a8f..6669d93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,7 +36,7 @@ MANIFEST
 pip-log.txt
 pip-delete-this-directory.txt
 
-# Unit test / coverage reports
+# Unit tests / coverage reports
 htmlcov/
 .tox/
 .nox/
@@ -169,3 +169,5 @@ history/*
 model.mdl
 
 data/
+
+junit/
diff --git a/Dockerfile b/Dockerfile
index dc16b12..db6c647 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,4 +9,4 @@ COPY . /app
 WORKDIR /app
 RUN pipenv install --system --deploy --ignore-pipfile
 
-CMD ["python", "main.py"]
\ No newline at end of file
+CMD ["python", "main.py"]
diff --git a/Pipfile.lock b/Pipfile.lock
index c2cc981..f640fc4 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -33,84 +33,121 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
-                "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
-                "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
-                "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
-                "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
-                "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
-                "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
-                "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
-                "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
-                "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
-                "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
-                "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
-                "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
-                "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
-                "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
-                "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
-                "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
-                "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
-                "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
-                "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
-                "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
-                "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
-                "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
-                "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
-                "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
-                "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
-                "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
-                "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
-                "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
-                "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
-                "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
-                "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
-                "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
-                "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
-                "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
-                "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
-                "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
-                "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
-                "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
-                "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
-                "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
-                "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
-                "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
-                "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
-                "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
-                "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
-                "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
-                "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
-                "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
-                "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
-                "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
-                "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
-                "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
-                "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
-                "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
-                "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
-                "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
-                "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
-                "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
-                "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
-                "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
-                "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
-                "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
-                "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
-                "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
-                "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
-                "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
-                "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
-                "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
-                "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
-                "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
-                "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
-                "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
-                "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
-                "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
+                "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843",
+                "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786",
+                "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e",
+                "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8",
+                "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4",
+                "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa",
+                "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d",
+                "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82",
+                "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7",
+                "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895",
+                "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d",
+                "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a",
+                "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382",
+                "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678",
+                "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b",
+                "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e",
+                "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741",
+                "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4",
+                "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596",
+                "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9",
+                "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69",
+                "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c",
+                "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77",
+                "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13",
+                "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459",
+                "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e",
+                "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7",
+                "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908",
+                "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a",
+                "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f",
+                "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8",
+                "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482",
+                "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d",
+                "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d",
+                "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545",
+                "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34",
+                "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86",
+                "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6",
+                "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe",
+                "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e",
+                "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc",
+                "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7",
+                "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd",
+                "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c",
+                "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557",
+                "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a",
+                "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89",
+                "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078",
+                "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e",
+                "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4",
+                "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403",
+                "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0",
+                "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89",
+                "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115",
+                "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9",
+                "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05",
+                "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a",
+                "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec",
+                "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56",
+                "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38",
+                "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479",
+                "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c",
+                "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e",
+                "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd",
+                "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186",
+                "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455",
+                "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c",
+                "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65",
+                "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78",
+                "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287",
+                "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df",
+                "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43",
+                "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1",
+                "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7",
+                "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989",
+                "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a",
+                "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63",
+                "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884",
+                "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649",
+                "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810",
+                "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828",
+                "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4",
+                "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2",
+                "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd",
+                "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5",
+                "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe",
+                "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293",
+                "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e",
+                "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e",
+                "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"
             ],
             "markers": "python_full_version >= '3.7.0'",
-            "version": "==3.2.0"
+            "version": "==3.3.0"
+        },
+        "cmake": {
+            "hashes": [
+                "sha256:009058bdf4f488709f38eaa5dd0ef0f89c6b9c6b6edd9d5b475a308ef75f80bb",
+                "sha256:1b9067ce0251cba3d4c018f2e1577ba9078e9c1eff6ad607ad5ce867843d4571",
+                "sha256:2fb48c780f1a6a3d19e785ebbb754be79d369e25a1cb81043fab049e709564da",
+                "sha256:499b38c77d52fb1964dbb38d0228fed246263a181939a8e753fde8ca227c8e1e",
+                "sha256:591f6b056527aefec009bc61a388776b2fc62444deb0038112a471031f61aeca",
+                "sha256:68983b09de633fc1ce6ab6bce9a25bfa181e41598e7c6bc0a6c0108773ee01cb",
+                "sha256:72289361866314f73be2ae63ddee224ff70223dcef9feb66d0072bf17e245564",
+                "sha256:7bf96237ba11ce2437dc5e071d96b510120a1be4708c631a64b2f38fb46bbd77",
+                "sha256:8056c99e371ff57229df2068364d7c32fea716cb53b4675f639edfb62663decf",
+                "sha256:810e592b606d05a3080a9c19ea839b13226f62cae447a22485b2365782f6b926",
+                "sha256:8bd1e1fa4fc8de7605c663d9408dceb649112f855aab05cca31fdb72e4d78364",
+                "sha256:9f4a7c7be2a25de5901f045618f41b833ea6c0f647115201d38e4fdf7e2815bc",
+                "sha256:b8a2fcb619b89d1cce7b52828316de9a1f27f0c90c2e39d1eae886428c8ee8c6",
+                "sha256:bd40d46dbad3555d5b3ce054bef24b85f256b19139493773751ab6f2b71c1219",
+                "sha256:bdbf0256f554f68c7b1d9740f5d059daf875b685c81a479cbe69038e84eb2fb9",
+                "sha256:c981aafcca2cd7210bd210ec75710c0f34e1fde1998cdcab812e4133e3ab615d",
+                "sha256:d582ef3e9ff0bd113581c1a32e881d1c2f9a34d2de76c93324a28593a76433db"
+            ],
+            "version": "==3.27.7"
         },
         "filelock": {
             "hashes": [
@@ -152,6 +189,12 @@
             "markers": "python_version >= '3.7'",
             "version": "==1.3.2"
         },
+        "lit": {
+            "hashes": [
+                "sha256:e6049032462be1e2928686cbd4a6cc5b3c545d83ecd078737fe79412c1f3fcc1"
+            ],
+            "version": "==17.0.3"
+        },
         "loguru": {
             "hashes": [
                 "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb",
@@ -247,6 +290,7 @@
                 "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
             ],
             "index": "pypi",
+            "markers": "python_version >= '3.5'",
             "version": "==1.0.0"
         },
         "networkx": {
@@ -293,8 +337,98 @@
                 "sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"
             ],
             "index": "pypi",
+            "markers": "python_version < '3.13' and python_version >= '3.9'",
             "version": "==1.26.0"
         },
+        "nvidia-cublas-cu11": {
+            "hashes": [
+                "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e",
+                "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.10.3.66"
+        },
+        "nvidia-cuda-cupti-cu11": {
+            "hashes": [
+                "sha256:7cc5b8f91ae5e1389c3c0ad8866b3b016a175e827ea8f162a672990a402ab2b0",
+                "sha256:e0cfd9854e1f2edaa36ca20d21cd0bdd5dcfca4e3b9e130a082e05b33b6c5895"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.7.101"
+        },
+        "nvidia-cuda-nvrtc-cu11": {
+            "hashes": [
+                "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03",
+                "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3",
+                "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.7.99"
+        },
+        "nvidia-cuda-runtime-cu11": {
+            "hashes": [
+                "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7",
+                "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.7.99"
+        },
+        "nvidia-cudnn-cu11": {
+            "hashes": [
+                "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7",
+                "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==8.5.0.96"
+        },
+        "nvidia-cufft-cu11": {
+            "hashes": [
+                "sha256:222f9da70c80384632fd6035e4c3f16762d64ea7a843829cb278f98b3cb7dd81",
+                "sha256:c4d316f17c745ec9c728e30409612eaf77a8404c3733cdf6c9c1569634d1ca03"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==10.9.0.58"
+        },
+        "nvidia-curand-cu11": {
+            "hashes": [
+                "sha256:eecb269c970fa599a2660c9232fa46aaccbf90d9170b96c462e13bcb4d129e2c",
+                "sha256:f742052af0e1e75523bde18895a9ed016ecf1e5aa0ecddfcc3658fd11a1ff417"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==10.2.10.91"
+        },
+        "nvidia-cusolver-cu11": {
+            "hashes": [
+                "sha256:00f70b256add65f8c1eb3b6a65308795a93e7740f6df9e273eccbba770d370c4",
+                "sha256:700b781bfefd57d161443aff9ace1878584b93e0b2cfef3d6e9296d96febbf99",
+                "sha256:72fa7261d755ed55c0074960df5904b65e2326f7adce364cbe4945063c1be412"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.4.0.1"
+        },
+        "nvidia-cusparse-cu11": {
+            "hashes": [
+                "sha256:304a01599534f5186a8ed1c3756879282c72c118bc77dd890dc1ff868cad25b9",
+                "sha256:a3389de714db63321aa11fbec3919271f415ef19fda58aed7f2ede488c32733d"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.7.4.91"
+        },
+        "nvidia-nccl-cu11": {
+            "hashes": [
+                "sha256:5e5534257d1284b8e825bc3a182c6f06acd6eb405e9f89d49340e98cd8f136eb"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==2.14.3"
+        },
+        "nvidia-nvtx-cu11": {
+            "hashes": [
+                "sha256:b22c64eee426a62fc00952b507d6d29cf62b4c9df7a480fcc417e540e05fd5ac",
+                "sha256:dfd7fcb2a91742513027d63a26b757f38dd8b07fecac282c4d132a9d373ff064"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==11.7.91"
+        },
         "pandas": {
             "hashes": [
                 "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614",
@@ -324,109 +458,110 @@
                 "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363"
             ],
             "index": "pypi",
+            "markers": "python_version >= '3.9'",
             "version": "==2.1.1"
         },
         "pillow": {
             "hashes": [
-                "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff",
-                "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f",
-                "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21",
-                "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635",
-                "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a",
-                "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f",
-                "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1",
-                "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d",
-                "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db",
-                "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849",
-                "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7",
-                "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876",
-                "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3",
-                "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317",
-                "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91",
-                "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d",
-                "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b",
-                "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd",
-                "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed",
-                "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500",
-                "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7",
-                "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a",
-                "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a",
-                "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0",
-                "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf",
-                "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f",
-                "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1",
-                "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088",
-                "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971",
-                "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a",
-                "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205",
-                "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54",
-                "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08",
-                "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21",
-                "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d",
-                "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08",
-                "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e",
-                "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf",
-                "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b",
-                "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145",
-                "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2",
-                "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d",
-                "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d",
-                "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf",
-                "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad",
-                "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d",
-                "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1",
-                "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4",
-                "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2",
-                "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19",
-                "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37",
-                "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4",
-                "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68",
-                "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"
+                "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d",
+                "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de",
+                "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616",
+                "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839",
+                "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099",
+                "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a",
+                "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219",
+                "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106",
+                "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b",
+                "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412",
+                "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b",
+                "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7",
+                "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2",
+                "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7",
+                "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14",
+                "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f",
+                "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27",
+                "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57",
+                "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262",
+                "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28",
+                "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610",
+                "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172",
+                "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273",
+                "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e",
+                "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d",
+                "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818",
+                "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f",
+                "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9",
+                "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01",
+                "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7",
+                "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651",
+                "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312",
+                "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80",
+                "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666",
+                "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061",
+                "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b",
+                "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992",
+                "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593",
+                "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4",
+                "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db",
+                "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba",
+                "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd",
+                "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e",
+                "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212",
+                "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb",
+                "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2",
+                "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34",
+                "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256",
+                "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f",
+                "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2",
+                "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38",
+                "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996",
+                "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a",
+                "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"
             ],
             "markers": "python_version >= '3.8'",
-            "version": "==10.0.1"
+            "version": "==10.1.0"
         },
         "pydantic": {
             "hashes": [
-                "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303",
-                "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe",
-                "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47",
-                "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494",
-                "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33",
-                "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86",
-                "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d",
-                "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c",
-                "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a",
-                "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565",
-                "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb",
-                "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62",
-                "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62",
-                "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0",
-                "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523",
-                "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d",
-                "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405",
-                "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f",
-                "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b",
-                "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718",
-                "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed",
-                "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb",
-                "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5",
-                "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc",
-                "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942",
-                "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe",
-                "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246",
-                "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350",
-                "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303",
-                "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09",
-                "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33",
-                "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8",
-                "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a",
-                "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1",
-                "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6",
-                "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"
+                "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548",
+                "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80",
+                "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340",
+                "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01",
+                "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132",
+                "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599",
+                "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1",
+                "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8",
+                "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe",
+                "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0",
+                "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17",
+                "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953",
+                "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f",
+                "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f",
+                "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d",
+                "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127",
+                "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8",
+                "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f",
+                "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580",
+                "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6",
+                "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691",
+                "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87",
+                "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd",
+                "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96",
+                "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687",
+                "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33",
+                "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69",
+                "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653",
+                "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78",
+                "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261",
+                "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f",
+                "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9",
+                "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d",
+                "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737",
+                "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5",
+                "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==1.10.12"
+            "version": "==1.10.13"
         },
         "pylemmy": {
             "hashes": [
@@ -434,6 +569,7 @@
                 "sha256:eea0a8b1896f827c2f6ca12d0ea6c958c35d88353b7ad197cfefae760d0680e2"
             ],
             "index": "pypi",
+            "markers": "python_version >= '3.7'",
             "version": "==0.0.6"
         },
         "python-dateutil": {
@@ -492,7 +628,9 @@
         },
         "scikit-learn": {
             "hashes": [
+                "sha256:0c275a06c5190c5ce00af0acbb61c06374087949f643ef32d355ece12c4db043",
                 "sha256:0ce9233cdf0cdcf0858a5849d306490bf6de71fa7603a3835124e386e62f2311",
+                "sha256:0e1aa8f206d0de814b81b41d60c1ce31f7f2c7354597af38fae46d9c47c45122",
                 "sha256:14e8775eba072ab10866a7e0596bc9906873e22c4c370a651223372eb62de180",
                 "sha256:1a231cced3ee3fa04756b4a7ab532dc9417acd581a330adff5f2c01ac2831fcf",
                 "sha256:1ec668ce003a5b3d12d020d2cde0abd64b262ac5f098b5c84cf9657deb9996a8",
@@ -506,47 +644,59 @@
                 "sha256:8454d57a22d856f1fbf3091bd86f9ebd4bff89088819886dc0c72f47a6c30652",
                 "sha256:845f81c7ceb4ea6bac64ab1c9f2ce8bef0a84d0f21f3bece2126adcc213dfecd",
                 "sha256:8d993fb70a1d78c9798b8f2f28705bfbfcd546b661f9e2e67aa85f81052b9c53",
+                "sha256:9147a3a4df4d401e618713880be023e36109c85d8569b3bf5377e6cd3fecdeac",
                 "sha256:a15d964d9eb181c79c190d3dbc2fff7338786bf017e9039571418a1d53dab236",
                 "sha256:a683394bc3f80b7c312c27f9b14ebea7766b1f0a34faf1a2e9158d80e860ec26",
                 "sha256:a7135a03af71138669f19bc96e7d0cc8081aed4b3565cc3b131135d65fc642ba",
                 "sha256:c413c2c850241998168bbb3bd1bb59ff03b1195a53864f0b80ab092071af6028",
                 "sha256:c6448c37741145b241eeac617028ba6ec2119e1339b1385c9720dae31367f2be",
                 "sha256:ccbbedae99325628c1d1cbe3916b7ef58a1ce949672d8d39c8b190e10219fd32",
+                "sha256:d2cd3634695ad192bf71645702b3df498bd1e246fc2d529effdb45a06ab028b4",
+                "sha256:ef540e09873e31569bc8b02c8a9f745ee04d8e1263255a15c9969f6f5caa627f",
                 "sha256:f66eddfda9d45dd6cadcd706b65669ce1df84b8549875691b1f403730bdef217"
             ],
             "index": "pypi",
+            "markers": "python_version >= '3.8'",
             "version": "==1.3.1"
         },
         "scipy": {
             "hashes": [
-                "sha256:0f3261f14b767b316d7137c66cc4f33a80ea05841b9c87ad83a726205b901423",
-                "sha256:10eb6af2f751aa3424762948e5352f707b0dece77288206f227864ddf675aca0",
-                "sha256:1342ca385c673208f32472830c10110a9dcd053cf0c4b7d4cd7026d0335a6c1d",
-                "sha256:214cdf04bbae7a54784f8431f976704ed607c4bc69ba0d5d5d6a9df84374df76",
-                "sha256:2b997a5369e2d30c97995dcb29d638701f8000d04df01b8e947f206e5d0ac788",
-                "sha256:2c91cf049ffb5575917f2a01da1da082fd24ed48120d08a6e7297dfcac771dcd",
-                "sha256:3aeb87661de987f8ec56fa6950863994cd427209158255a389fc5aea51fa7055",
-                "sha256:4447ad057d7597476f9862ecbd9285bbf13ba9d73ce25acfa4e4b11c6801b4c9",
-                "sha256:542a757e2a6ec409e71df3d8fd20127afbbacb1c07990cb23c5870c13953d899",
-                "sha256:8d9886f44ef8c9e776cb7527fb01455bf4f4a46c455c4682edc2c2cc8cd78562",
-                "sha256:90d3b1364e751d8214e325c371f0ee0dd38419268bf4888b2ae1040a6b266b2a",
-                "sha256:95763fbda1206bec41157582bea482f50eb3702c85fffcf6d24394b071c0e87a",
-                "sha256:ac74b1512d38718fb6a491c439aa7b3605b96b1ed3be6599c17d49d6c60fca18",
-                "sha256:afdb0d983f6135d50770dd979df50bf1c7f58b5b33e0eb8cf5c73c70600eae1d",
-                "sha256:b0620240ef445b5ddde52460e6bc3483b7c9c750275369379e5f609a1050911c",
-                "sha256:b133f237bd8ba73bad51bc12eb4f2d84cbec999753bf25ba58235e9fc2096d80",
-                "sha256:b29318a5e39bd200ca4381d80b065cdf3076c7d7281c5e36569e99273867f61d",
-                "sha256:b8425fa963a32936c9773ee3ce44a765d8ff67eed5f4ac81dc1e4a819a238ee9",
-                "sha256:d2b813bfbe8dec6a75164523de650bad41f4405d35b0fa24c2c28ae07fcefb20",
-                "sha256:d690e1ca993c8f7ede6d22e5637541217fc6a4d3f78b3672a6fe454dbb7eb9a7",
-                "sha256:e367904a0fec76433bf3fbf3e85bf60dae8e9e585ffd21898ab1085a29a04d16",
-                "sha256:ea932570b1c2a30edafca922345854ff2cd20d43cd9123b6dacfdecebfc1a80b",
-                "sha256:f28f1f6cfeb48339c192efc6275749b2a25a7e49c4d8369a28b6591da02fbc9a",
-                "sha256:f73102f769ee06041a3aa26b5841359b1a93cc364ce45609657751795e8f4a4a",
-                "sha256:fa4909c6c20c3d91480533cddbc0e7c6d849e7d9ded692918c76ce5964997898"
+                "sha256:00f325434b6424952fbb636506f0567898dca7b0f7654d48f1c382ea338ce9a3",
+                "sha256:033c3fd95d55012dd1148b201b72ae854d5086d25e7c316ec9850de4fe776929",
+                "sha256:0d3a136ae1ff0883fffbb1b05b0b2fea251cb1046a5077d0b435a1839b3e52b7",
+                "sha256:15f237e890c24aef6891c7d008f9ff7e758c6ef39a2b5df264650eb7900403c0",
+                "sha256:370f569c57e1d888304052c18e58f4a927338eafdaef78613c685ca2ea0d1fa0",
+                "sha256:3e1a8a4657673bfae1e05e1e1d6e94b0cabe5ed0c7c144c8aa7b7dbb774ce5c1",
+                "sha256:4b4bb134c7aa457e26cc6ea482b016fef45db71417d55cc6d8f43d799cdf9ef2",
+                "sha256:5305792c7110e32ff155aed0df46aa60a60fc6e52cd4ee02cdeb67eaccd5356e",
+                "sha256:5664e364f90be8219283eeb844323ff8cd79d7acbd64e15eb9c46b9bc7f6a42a",
+                "sha256:5f290cf561a4b4edfe8d1001ee4be6da60c1c4ea712985b58bf6bc62badee221",
+                "sha256:74e89dc5e00201e71dd94f5f382ab1c6a9f3ff806c7d24e4e90928bb1aafb280",
+                "sha256:7abda0e62ef00cde826d441485e2e32fe737bdddee3324e35c0e01dee65e2a88",
+                "sha256:90271dbde4be191522b3903fc97334e3956d7cfb9cce3f0718d0ab4fd7d8bfd6",
+                "sha256:91770cb3b1e81ae19463b3c235bf1e0e330767dca9eb4cd73ba3ded6c4151e4d",
+                "sha256:925c6f09d0053b1c0f90b2d92d03b261e889b20d1c9b08a3a51f61afc5f58165",
+                "sha256:9885e3e4f13b2bd44aaf2a1a6390a11add9f48d5295f7a592393ceb8991577a3",
+                "sha256:9ea7f579182d83d00fed0e5c11a4aa5ffe01460444219dedc448a36adf0c3917",
+                "sha256:a63d1ec9cadecce838467ce0631c17c15c7197ae61e49429434ba01d618caa83",
+                "sha256:bae66a2d7d5768eaa33008fa5a974389f167183c87bf39160d3fefe6664f8ddc",
+                "sha256:bba4d955f54edd61899776bad459bf7326e14b9fa1c552181f0479cc60a568cd",
+                "sha256:c77da50c9a91e23beb63c2a711ef9e9ca9a2060442757dffee34ea41847d8156",
+                "sha256:d2f6dee6cbb0e263b8142ed587bc93e3ed5e777f1f75448d24fb923d9fd4dce6",
+                "sha256:dfcc1552add7cb7c13fb70efcb2389d0624d571aaf2c80b04117e2755a0c5d15",
+                "sha256:e04aa19acc324a1a076abb4035dabe9b64badb19f76ad9c798bde39d41025cdc",
+                "sha256:e1f97cd89c0fe1a0685f8f89d85fa305deb3067d0668151571ba50913e445820"
             ],
             "markers": "python_version < '3.13' and python_version >= '3.9'",
-            "version": "==1.11.2"
+            "version": "==1.11.3"
+        },
+        "setuptools": {
+            "hashes": [
+                "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
+                "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==68.2.2"
         },
         "six": {
             "hashes": [
@@ -596,6 +746,7 @@
                 "sha256:f66aa6b9580a22b04d0af54fcd042f52406a8479e2b6a550e3d9f95963e168c8"
             ],
             "index": "pypi",
+            "markers": "python_full_version >= '3.8.0'",
             "version": "==2.0.1"
         },
         "torchdata": {
@@ -645,6 +796,7 @@
                 "sha256:fe1794f9b2a9f4923f87488783b06d6c683f332a1db60d17cf65ba3f7c724c56"
             ],
             "index": "pypi",
+            "markers": "python_version >= '3.8'",
             "version": "==0.15.2"
         },
         "tqdm": {
@@ -655,6 +807,30 @@
             "markers": "python_version >= '3.7'",
             "version": "==4.66.1"
         },
+        "triton": {
+            "hashes": [
+                "sha256:0117722f8c2b579cd429e0bee80f7731ae05f63fe8e9414acd9a679885fcbf42",
+                "sha256:1aca3303629cd3136375b82cb9921727f804e47ebee27b2677fef23005c3851a",
+                "sha256:226941c7b8595219ddef59a1fdb821e8c744289a132415ddd584facedeb475b1",
+                "sha256:38806ee9663f4b0f7cd64790e96c579374089e58f49aac4a6608121aa55e2505",
+                "sha256:42a0d2c3fc2eab4ba71384f2e785fbfd47aa41ae05fa58bf12cb31dcbd0aeceb",
+                "sha256:47b4d70dc92fb40af553b4460492c31dc7d3a114a979ffb7a5cdedb7eb546c08",
+                "sha256:4c9fc8c89874bc48eb7e7b2107a9b8d2c0bf139778637be5bfccb09191685cfd",
+                "sha256:52c47b72c72693198163ece9d90a721299e4fb3b8e24fd13141e384ad952724f",
+                "sha256:74f118c12b437fb2ca25e1a04759173b517582fcf4c7be11913316c764213656",
+                "sha256:75834f27926eab6c7f00ce73aaf1ab5bfb9bec6eb57ab7c0bfc0a23fac803b4c",
+                "sha256:8f05a7e64e4ca0565535e3d5d3405d7e49f9d308505bb7773d21fb26a4c008c2",
+                "sha256:9618815a8da1d9157514f08f855d9e9ff92e329cd81c0305003eb9ec25cc5add",
+                "sha256:9d4978298b74fcf59a75fe71e535c092b023088933b2f1df933ec32615e4beef",
+                "sha256:bb4b99ca3c6844066e516658541d876c28a5f6e3a852286bbc97ad57134827fd",
+                "sha256:bcd9be5d0c2e45d2b7e6ddc6da20112b6862d69741576f9c3dbaf941d745ecae",
+                "sha256:d2684b6a60b9f174f447f36f933e9a45f31db96cb723723ecd2dcfd1c57b778b",
+                "sha256:e3e13aa8b527c9b642e3a9defcc0fbd8ffbe1c80d8ac8c15a01692478dc64d8a",
+                "sha256:fedce6a381901b1547e0e7e1f2546e4f65dca6d91e2d8a7305a2d1f5551895be"
+            ],
+            "markers": "platform_system == 'Linux' and platform_machine == 'x86_64'",
+            "version": "==2.0.0"
+        },
         "typing-extensions": {
             "hashes": [
                 "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
@@ -673,11 +849,12 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
-                "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
+                "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07",
+                "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"
             ],
+            "index": "pypi",
             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==1.26.16"
+            "version": "==1.26.18"
         },
         "wheel": {
             "hashes": [
@@ -685,6 +862,7 @@
                 "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"
             ],
             "index": "pypi",
+            "markers": "python_version >= '3.7'",
             "version": "==0.41.2"
         }
     },
diff --git a/config.py b/config.py
deleted file mode 100644
index f42bb58..0000000
--- a/config.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Rename this file to config.py and update the values below.
-import os
-
-username = os.getenv("LEMMY_USERNAME")
-password = os.getenv("LEMMY_PASSWORD")
-instance = os.getenv("LEMMY_INSTANCE")
-owner_username = os.getenv("LEMMY_OWNER_USERNAME")
-communities = [x.strip() for x in os.getenv("LEMMY_COMMUNITIES").split(',')]
-debug_mode = False  # Setting this value to true will mean that the bot will not actually submit reports.
-
-# Matrix login details for the bot to communicate with a Matrix room
-"""    
-    server : string - The name of the server.  e.g. "https://matrix.example.org"
-    account : string - The name of the account e.g. "@alice:example.org"
-    password : string - The password for the account e.g. "my-secret-password"
-    room_id : string - The id of the room e.g. "!my-fave-room:example.org"
-    content : string - The content of the message e.g. "Hello World!"
-"""
-matrix_server = os.getenv("MATRIX_INSTANCE")
-matrix_account = os.getenv("MATRIX_USERNAME")
-matrix_password = os.getenv("MATRIX_PASSWORD")
-matrix_room_id = os.getenv("MATRIX_ROOM")
-
-
diff --git a/lemmymodbot/__init__.py b/lemmymodbot/__init__.py
new file mode 100644
index 0000000..fb514ed
--- /dev/null
+++ b/lemmymodbot/__init__.py
@@ -0,0 +1,5 @@
+from .api import *
+from .processors import *
+from .lemmybot import LemmyBot
+from .config import Config, MatrixConfig, environment_config
+from .ml import ToxicityClassifier
diff --git a/api/__init__.py b/lemmymodbot/api/__init__.py
similarity index 100%
rename from api/__init__.py
rename to lemmymodbot/api/__init__.py
diff --git a/api/endpoints.py b/lemmymodbot/api/endpoints.py
similarity index 100%
rename from api/endpoints.py
rename to lemmymodbot/api/endpoints.py
diff --git a/api/lemmy_mod_http.py b/lemmymodbot/api/lemmy_mod_http.py
similarity index 95%
rename from api/lemmy_mod_http.py
rename to lemmymodbot/api/lemmy_mod_http.py
index cfe2484..5e5667e 100644
--- a/api/lemmy_mod_http.py
+++ b/lemmymodbot/api/lemmy_mod_http.py
@@ -5,7 +5,7 @@
 from pylemmy.api.utils import BaseApiModel
 from pylemmy.endpoints import LemmyAPI
 
-from api import LemmyModAPI, PostMessage, PostRemovePost, PostRemoveComment
+from lemmymodbot.api import LemmyModAPI, PostMessage, PostRemovePost, PostRemoveComment
 
 
 class LemmyModHttp:
diff --git a/api/message.py b/lemmymodbot/api/message.py
similarity index 100%
rename from api/message.py
rename to lemmymodbot/api/message.py
diff --git a/lemmymodbot/config.py b/lemmymodbot/config.py
new file mode 100644
index 0000000..d078554
--- /dev/null
+++ b/lemmymodbot/config.py
@@ -0,0 +1,67 @@
+# Rename this file to config.py and update the values below.
+import os
+from typing import List
+
+
+class MatrixConfig:
+    server: str
+    account: str
+    password: str
+    room_id: str
+
+    def __init__(
+            self,
+            server: str,
+            account: str,
+            password: str,
+            room_id: str,
+    ):
+        self.server = server
+        self.account = account
+        self.password = password
+        self.room_id = room_id
+
+
+class Config:
+    username: str
+    password: str
+    instance: str
+    owner_username: str
+    communities: List[str]
+    debug_mode: bool
+    matrix_config: MatrixConfig
+
+    def __init__(
+            self,
+            username: str,
+            password: str,
+            instance: str,
+            owner_username: str,
+            communities: List[str],
+            matrix_config: MatrixConfig = None,
+            debug_mode: bool = False,
+    ):
+        self.username = username
+        self.password = password
+        self.instance = instance
+        self.owner_username = owner_username
+        self.communities = communities
+        self.debug_mode = debug_mode
+        self.matrix_config = matrix_config
+
+
+def environment_config():
+    return Config(
+        os.getenv("LEMMY_USERNAME"),
+        os.getenv("LEMMY_PASSWORD"),
+        os.getenv("LEMMY_INSTANCE"),
+        os.getenv("LEMMY_OWNER_USERNAME"),
+        [x.strip() for x in os.getenv("LEMMY_COMMUNITIES").split(',')],
+        MatrixConfig(
+            os.getenv("MATRIX_INSTANCE"),
+            os.getenv("MATRIX_USERNAME"),
+            os.getenv("MATRIX_PASSWORD"),
+            os.getenv("MATRIX_ROOM")
+        ) if os.getenv("MATRIX_INSTANCE") is not None else None,
+        True
+    )
\ No newline at end of file
diff --git a/database.py b/lemmymodbot/database.py
similarity index 86%
rename from database.py
rename to lemmymodbot/database.py
index cde0678..608f7f6 100644
--- a/database.py
+++ b/lemmymodbot/database.py
@@ -2,6 +2,7 @@
 from contextlib import contextmanager
 from pathlib import Path
 import sqlite3
+from sqlite3 import Error
 from typing import Optional, List
 
 
@@ -48,7 +49,7 @@ def _session(self):
         try:
             yield session
             session.commit()
-        except:
+        except Error:
             session.rollback()
             raise
         finally:
@@ -61,7 +62,7 @@ def check_table_exists(self, table_name, create_sql):
             # create cursor object
             cur = conn.cursor()
             # generate list of tables with the name of the table
-            sql = f"""SELECT tbl_name FROM sqlite_master WHERE type='table'
+            sql = """SELECT tbl_name FROM sqlite_master WHERE type='table'
                 AND tbl_name=?; """
             list_of_tables = cur.execute(sql, (table_name,)).fetchall()
             if len(list_of_tables) == 0:
@@ -72,14 +73,14 @@ def update_table(self, sql):
         with self._session() as conn:
             try:
                 conn.execute(sql)
-            except:
+            except Error:
                 pass
 
     def in_comments_list(self, comment_id):
         """check whether this comment (comment_id) is stored in
         the database as having been previously checked"""
         with self._session() as conn:
-            result = conn.execute(f'SELECT count(id) FROM comments WHERE id=?;', (comment_id,))
+            result = conn.execute('SELECT count(id) FROM comments WHERE id=?;', (comment_id,))
             for row in result.fetchone():
                 if row != 0:
                     return True
@@ -89,7 +90,7 @@ def in_posts_list(self, post_id):
         """ check whether this post (id) is stored in
         the database as having been previously checked"""
         with self._session() as conn:
-            result = conn.execute(f'SELECT count(id) FROM posts WHERE id=?;', (post_id,))
+            result = conn.execute('SELECT count(id) FROM posts WHERE id=?;', (post_id,))
             for row in result.fetchone():
                 if row != 0:
                     return True
@@ -98,7 +99,7 @@ def in_posts_list(self, post_id):
     def add_to_comments_list(self, comment_id, results):
         """ add a comment id to the list of previously checked comments in the database """
         with self._session() as conn:
-            sql = f'''INSERT INTO comments(id, toxicity, non_toxicity) VALUES(?,?,?);'''
+            sql = '''INSERT INTO comments(id, toxicity, non_toxicity) VALUES(?,?,?);'''
             conn.execute(sql, (comment_id, results['toxicity'], results['non_toxicity']))
 
     def add_outcome_to_comment(self, comment_id, outcome):
@@ -107,20 +108,20 @@ def add_outcome_to_comment(self, comment_id, outcome):
             # tidy up the outcome string to remove quotes which might break the SQL statement
             outcome = outcome.replace('"', '')
             outcome = outcome.replace("'", "")
-            sql = f'''UPDATE comments SET outcome=? WHERE id=?;'''
+            sql = '''UPDATE comments SET outcome=? WHERE id=?;'''
             conn.execute(sql, (outcome, comment_id))
 
     def add_outcome_to_post(self, post_id, outcome):
         """ add an outcome to a post record in the database """
 
         with self._session() as conn:
-            sql = f'''UPDATE posts SET outcome=? WHERE id=?;'''
+            sql = '''UPDATE posts SET outcome=? WHERE id=?;'''
             conn.execute(sql, (outcome, post_id))
 
     def add_to_posts_list(self, post_id, detox_name_results, detox_body_results, extras, link):
         """ add a post id to the list of previously checked posts """
         with self._session() as conn:
-            sql = f"""INSERT INTO posts(id, name_toxicity, name_non_toxicity, 
+            sql = """INSERT INTO posts(id, name_toxicity, name_non_toxicity, 
             body_toxicity, body_non_toxicity, link, phash) VALUES(?,?,?,?,?,?,?);"""
 
             conn.execute(sql, (post_id,
@@ -134,12 +135,12 @@ def add_to_posts_list(self, post_id, detox_name_results, detox_body_results, ext
 
     def add_phash(self, url: str, phash: str):
         with self._session() as conn:
-            sql = f"""INSERT INTO phash(url, phash) VALUES(?,?);"""
+            sql = """INSERT INTO phash(url, phash) VALUES(?,?);"""
             conn.execute(sql, (url, phash))
 
     def phash_exists(self, phash: str):
         with self._session() as conn:
-            sql = f"""SELECT COUNT(url) FROM phash where phash=?"""
+            sql = """SELECT COUNT(url) FROM phash where phash=?"""
             result = conn.execute(sql, (phash,))
             for row in result.fetchone():
                 if row != 0:
@@ -148,7 +149,7 @@ def phash_exists(self, phash: str):
 
     def url_exists(self, url: str) -> Optional[str]:
         with self._session() as conn:
-            sql = f"""SELECT phash FROM phash where url=?"""
+            sql = """SELECT phash FROM phash where url=?"""
             result = conn.execute(sql, (url,))
             for row in result.fetchall():
                 if row[0] is not None:
@@ -157,6 +158,6 @@ def url_exists(self, url: str) -> Optional[str]:
 
     def get_post_links_by_phash(self, phash: str) -> List[str]:
         with self._session() as conn:
-            sql = f"""SELECT link FROM posts WHERE phash=? LIMIT 5"""
+            sql = """SELECT link FROM posts WHERE phash=? LIMIT 5"""
             result = conn.execute(sql, (phash,))
-            return [row[0] for row in result.fetchall()]
\ No newline at end of file
+            return [row[0] for row in result.fetchall()]
diff --git a/lemmybot.py b/lemmymodbot/lemmybot.py
similarity index 84%
rename from lemmybot.py
rename to lemmymodbot/lemmybot.py
index b074124..6660ee1 100644
--- a/lemmybot.py
+++ b/lemmymodbot/lemmybot.py
@@ -12,18 +12,24 @@
 from pylemmy.models.comment import Comment
 from matrix_client.client import MatrixClient
 
-import config
-from processors.base import Processor, Content, ContentType, LemmyHandle
-from reconnection_manager import ReconnectionDelayManager
-from database import Database
+from .config import Config, environment_config
+from lemmymodbot.processors.base import Processor, Content, ContentType, LemmyHandle
+from .reconnection_manager import ReconnectionDelayManager
+from .database import Database
 
 
 class LemmyBot:
     processors: List[Processor]
+    config: Config
     """ LemmyBot is a bot that checks Lemmy posts and comments for toxicity, as well as
     performing regexp matching, user watchlist monitoring amongst other things."""
 
-    def __init__(self, processors: List[Processor]):
+    def __init__(
+            self,
+            processors: List[Processor],
+            config: Config = None
+    ):
+        self.config = config if config is not None else environment_config()
         self.processors = processors
         self.logger = logging.getLogger()
         self.logger.setLevel(logging.DEBUG)
@@ -39,12 +45,12 @@ def __init__(self, processors: List[Processor]):
             processor.setup()
 
         self.lemmy = Lemmy(
-            lemmy_url=config.instance,
-            username=config.username,
-            password=config.password,
-            user_agent="custom user agent (by " + config.owner_username + ")",
+            lemmy_url=self.config.instance,
+            username=self.config.username,
+            password=self.config.password,
+            user_agent="custom user agent (by " + self.config.owner_username + ")",
         )
-        
+
         db_directory_name = 'data'
         db_file_name = 'history.db'
         self.history_db = Database(db_directory_name, db_file_name)
@@ -134,13 +140,14 @@ def process_comment(self, elem):
             if len(flags) > 0:
                 # we found something bad
                 self.logger.info('REPORT FOR COMMENT: %s', flags)
+                # noinspection PyBroadException
                 try:
-                    if not config.debug_mode:
+                    if not self.config.debug_mode:
                         elem.create_report(reason='Mod bot (with L plates) : ' + ', '.join(flags))
                     self.logger.info('****************\nREPORTED COMMENT\n******************')
                     self.history_db.add_outcome_to_comment(comment_id, "Reported comment for: "
                                                            + '|'.join(flags))
-                except:
+                except Exception:
                     self.logger.error("ERROR: UNABLE TO CREATE REPORT", exc_info=True)
                     self.history_db.add_outcome_to_comment(comment_id, "Failed to report comment for: "
                                                            + '|'.join(flags) + " due to exception :"
@@ -148,10 +155,10 @@ def process_comment(self, elem):
                 matrix_message = '\n\nMod bot (with L plates) : ' + ', '.join(flags)
                 matrix_message = matrix_message + '\n' + str(extras)
                 matrix_message = matrix_message + '\n' + str(elem.comment_view.comment)
-                self.send_message_to_matrix(m_server=config.matrix_server,
-                                            m_account=config.matrix_account,
-                                            m_password=config.matrix_password,
-                                            m_room_id=config.matrix_room_id,
+                self.send_message_to_matrix(m_server=self.config.matrix_config.server,
+                                            m_account=self.config.matrix_config.account,
+                                            m_password=self.config.matrix_config.password,
+                                            m_room_id=self.config.matrix_config.room_id,
                                             m_content=matrix_message)
             else:
                 self.history_db.add_outcome_to_comment(comment_id, "No report")
@@ -202,18 +209,19 @@ def process_post(self, elem):
                 extras_title,
                 extras_body,
                 extras,
-                f"{config.instance}/post/{elem.post_view.post.id}"
+                f"{self.config.instance}/post/{elem.post_view.post.id}"
             )
             pprint(elem)
             if len(flags) > 0:
                 self.logger.info('REPORT FOR POST: %s', flags)
+                # noinspection PyBroadException
                 try:
-                    if not config.debug_mode:
+                    if not self.config.debug_mode:
                         elem.create_report(reason='Mod bot (with L plates) : ' + ', '.join(flags))
                     self.logger.info('****************\nREPORTED POST\n******************')
                     self.history_db.add_outcome_to_post(post_id, "Reported Post for: "
                                                         + '|'.join(flags))
-                except:
+                except Exception:
                     self.logger.error("ERROR: UNABLE TO CREATE REPORT", exc_info=True)
                     self.history_db.add_outcome_to_comment(post_id, "Failed to report post for: "
                                                            + '|'.join(flags) + " due to exception :"
@@ -222,10 +230,10 @@ def process_post(self, elem):
                 matrix_message = matrix_message + '\n' + str(extras_title)
                 matrix_message = matrix_message + '\n' + str(extras_body)
                 matrix_message = matrix_message + '\n' + str(elem.post_view.post)
-                self.send_message_to_matrix(m_server=config.matrix_server,
-                                            m_account=config.matrix_account,
-                                            m_password=config.matrix_password,
-                                            m_room_id=config.matrix_room_id,
+                self.send_message_to_matrix(m_server=self.config.matrix_config.server,
+                                            m_account=self.config.matrix_config.account,
+                                            m_password=self.config.matrix_config.password,
+                                            m_room_id=self.config.matrix_config.room_id,
                                             m_content=matrix_message)
             else:
                 self.history_db.add_outcome_to_post(post_id, "No report")
@@ -249,9 +257,10 @@ def process_content(self, elem: Union[Post, Comment]):
     def run(self):
         """This is the main run loop for the bot.  It should be called after initiation of bot"""
         while True:
+            # noinspection PyBroadException
             try:
-                multi_stream = self.lemmy.multi_communities_stream(config.communities)
+                multi_stream = self.lemmy.multi_communities_stream(self.config.communities)
                 multi_stream.content_apply(self.process_content)
-            except:
+            except Exception:
                 self.logger.error("Exception raised!", exc_info=True)
                 self.mydelay.wait()
diff --git a/lemmymodbot/ml/__init__.py b/lemmymodbot/ml/__init__.py
new file mode 100644
index 0000000..936c81b
--- /dev/null
+++ b/lemmymodbot/ml/__init__.py
@@ -0,0 +1,2 @@
+from .bag_of_words import BagOfWords
+from .classifier import ToxicityClassifier
\ No newline at end of file
diff --git a/bag_of_words.py b/lemmymodbot/ml/bag_of_words.py
similarity index 98%
rename from bag_of_words.py
rename to lemmymodbot/ml/bag_of_words.py
index 1b67e30..57e1a59 100644
--- a/bag_of_words.py
+++ b/lemmymodbot/ml/bag_of_words.py
@@ -145,7 +145,7 @@ def multi_hot_data(example, num_classes):
     # train_dataset = torch.tensor(train_data)
     # valid_dataset = torch.tensor(valid_data)
     # test_dataset = torch.tensor(test_data)
-    # test
+    # tests
 
     # train_tensor = torch.tensor(train_data)
     # valid_tensor = torch.tensor(valid_data)
@@ -211,7 +211,7 @@ def train(model, dataloader, loss_fn, optimizer, device):
             accuracies.append(accuracy.detach().cpu().numpy())
         return np.mean(losses), np.mean(accuracies)
 
-    # Define function `evaluate`, which we will use to evaluate the model with the validation and test sets.
+    # Define function `evaluate`, which we will use to evaluate the model with the validation and tests sets.
 
     def evaluate(model, dataloader, loss_fn, device):
         model.eval()
@@ -267,7 +267,7 @@ def evaluate(model, dataloader, loss_fn, device):
         #     valid_loss, valid_accuracy, confusion_mat = evaluate(model, valid_dataloader, loss_fn, device)
         valid_loss, valid_accuracy = evaluate(model, valid_dataloader, loss_fn, device)
         ##
-        # The following  line was commented until the final model was trained and the results with the test data were evaluated.
+        # The following  line was commented until the final model was trained and the results with the tests data were evaluated.
         ##
         # test_loss, test_accuracy, test_confusion_mat = evaluate(model, test_dataloader, loss_fn, device)
 
@@ -277,7 +277,7 @@ def evaluate(model, dataloader, loss_fn, device):
         valid_losses.append(valid_loss)
         valid_accuracies.append(valid_accuracy)
         ##
-        # The following 2 lines were commented until the final model was trained and the results with the test data were evaluated.
+        # The following 2 lines were commented until the final model was trained and the results with the tests data were evaluated.
         ##
         # test_losses.append(test_loss)
         # test_accuracies.append(test_accuracy)
diff --git a/processors/toxicity_processor.py b/lemmymodbot/ml/classifier.py
similarity index 56%
rename from processors/toxicity_processor.py
rename to lemmymodbot/ml/classifier.py
index 89229de..0f66d46 100644
--- a/processors/toxicity_processor.py
+++ b/lemmymodbot/ml/classifier.py
@@ -1,5 +1,4 @@
-from time import sleep
-from typing import List, Any, Optional, Union
+from typing import Any
 
 import numpy as np
 import pandas as pd
@@ -9,16 +8,12 @@
 from torch.types import Device
 from torchtext.vocab import Vocab
 
-import config
-from bag_of_words import BagOfWords, build_bow_model
-from processors import Processor, Content, LemmyHandle, ContentResult
-from processors.base import ContentType
+from .bag_of_words import build_bow_model, BagOfWords
 
 
-class ToxicityProcessor(Processor):
+class ToxicityClassifier:
     train_filename: str
     rebuild_model: bool
-    uncertainty_allowance = 0.2
     device: Device
     tokenizer: Any
     all_data: DataFrame
@@ -65,41 +60,19 @@ def setup(self) -> None:
         self.model.load_state_dict(torch.load("./data/model.mdl"))
         self.model.eval()
 
-    def execute(self, content: Content, handle: LemmyHandle) -> ContentResult:
-        if content.type == ContentType.POST_LINK:
-            return ContentResult.nothing()
-
-        local_flags = []
+    def classify(self, content):
         my_tokens = {}
-        if content is not None:
-            my_tokens['tokens'] = self.tokenizer(content.content)
-            my_numericals = {}
-            my_numericals['ids'] = self.numericalize_data(my_tokens)
-            my_multi_hot = self.multi_hot_data(my_numericals)
-            inputs = torch.tensor(my_multi_hot).to(self.device)
-            # if inputs.dtype != torch.float64:
-            #     inputs = inputs.float()
-            preds = self.model(inputs)
-            preds2 = (1 - torch.argmax(preds, dim=-1)).item()
-            preds3 = abs(preds[0].item() - preds[1].item())
-
-            print('\n\n' + content.content)
-            print(f'{preds}', (1 - torch.argmax(preds)).item())
-            if preds[0].item() < 0.2 and preds[1].item() < 0.2:
-                print("Low values^")
-            if abs(preds[0].item() - preds[1].item()) < self.uncertainty_allowance:
-                print("Close values^")
-            if preds2 == 1 and preds3 >= self.uncertainty_allowance:
-                sleep(15)
-                print("Toxic^")
-                local_flags.append('potentially toxic')
-
-            return ContentResult(local_flags, {
-                "toxicity": preds[0].item(),
-                "non_toxicity": preds[1].item(),
-            })
-        else:
-            return ContentResult.nothing()
+
+        my_tokens['tokens'] = self.tokenizer(content.content)
+        my_numericals = {'ids': self.numericalize_data(my_tokens)}
+        my_multi_hot = self.multi_hot_data(my_numericals)
+        inputs = torch.tensor(my_multi_hot).to(self.device)
+        # if inputs.dtype != torch.float64:
+        #     inputs = inputs.float()
+        preds = self.model(inputs)
+        preds2 = (1 - torch.argmax(preds, dim=-1)).item()
+        preds3 = abs(preds[0].item() - preds[1].item())
+        return (preds, preds2, preds3)
 
     def numericalize_data(self, example):
         """ Convert tokens into vocabulary index."""
@@ -111,4 +84,4 @@ def multi_hot_data(self, example):
         num_classes = len(self.vocab)
         encoded = np.zeros((num_classes,))
         encoded[example['ids']] = 1
-        return encoded
\ No newline at end of file
+        return encoded
diff --git a/processors/__init__.py b/lemmymodbot/processors/__init__.py
similarity index 95%
rename from processors/__init__.py
rename to lemmymodbot/processors/__init__.py
index 310449f..8e970a9 100644
--- a/processors/__init__.py
+++ b/lemmymodbot/processors/__init__.py
@@ -1,4 +1,4 @@
-from .base import Processor, LemmyHandle, Content, ContentResult
+from .base import Processor, LemmyHandle, Content, ContentResult, ContentType
 from .user_processor import UserProcessor
 from .toxicity_processor import ToxicityProcessor
 from .blacklist_processor import BlacklistProcessor
diff --git a/processors/base.py b/lemmymodbot/processors/base.py
similarity index 85%
rename from processors/base.py
rename to lemmymodbot/processors/base.py
index beb7a7e..8e3fc45 100644
--- a/processors/base.py
+++ b/lemmymodbot/processors/base.py
@@ -1,13 +1,16 @@
+from io import BytesIO
 from typing import List, Any, Optional, Union
-import torchtext
 from pylemmy import Lemmy
 from pylemmy.models.comment import Comment
 from pylemmy.models.post import Post
-import re
 
-import config
-from api import LemmyModHttp
-from database import Database
+from lemmymodbot import config
+from lemmymodbot.api import LemmyModHttp
+from lemmymodbot.database import Database
+
+import requests
+from PIL import Image
+import imagehash
 
 
 class LemmyHandle:
@@ -40,6 +43,10 @@ def remove_thing(self, reason: str):
         elif isinstance(self.elem, Comment):
             self.lemmy_http.remove_comment(self.elem.comment_view.comment.id, reason)
 
+    def fetch_image(self, url: str) -> (Image, str):
+        img = Image.open(BytesIO(requests.get(url).content))
+        return img, str(imagehash.phash(img))
+
 
 class ContentType:
     POST_TITLE = 0
diff --git a/processors/blacklist_processor.py b/lemmymodbot/processors/blacklist_processor.py
similarity index 83%
rename from processors/blacklist_processor.py
rename to lemmymodbot/processors/blacklist_processor.py
index 710d26a..6d58b1e 100644
--- a/processors/blacklist_processor.py
+++ b/lemmymodbot/processors/blacklist_processor.py
@@ -2,8 +2,8 @@
 
 import torchtext
 
-from processors import Processor, Content, LemmyHandle, ContentResult
-from processors.base import ContentType
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
 
 
 class BlacklistProcessor(Processor):
diff --git a/processors/phash_processor.py b/lemmymodbot/processors/phash_processor.py
similarity index 79%
rename from processors/phash_processor.py
rename to lemmymodbot/processors/phash_processor.py
index f007328..a7e06c8 100644
--- a/processors/phash_processor.py
+++ b/lemmymodbot/processors/phash_processor.py
@@ -1,11 +1,5 @@
-from io import BytesIO
-
-import requests
-from PIL import Image
-import imagehash
-
-from processors import Processor, Content, LemmyHandle, ContentResult
-from processors.base import ContentType
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
 
 
 class PhashProcessor(Processor):
@@ -18,7 +12,7 @@ def execute(self, content: Content, handle: LemmyHandle) -> ContentResult:
         if phash is not None:
             return self._warn_user(handle, phash)
 
-        phash = str(imagehash.phash(Image.open(BytesIO(requests.get(content.content).content))))
+        phash = handle.fetch_image(content.content)[1]
         if handle.database.phash_exists(phash):
             handle.database.add_phash(content.content, phash)
             return self._warn_user(handle, phash)
diff --git a/processors/title_conformity_processor.py b/lemmymodbot/processors/title_conformity_processor.py
similarity index 79%
rename from processors/title_conformity_processor.py
rename to lemmymodbot/processors/title_conformity_processor.py
index 4635fc3..8db81a0 100644
--- a/processors/title_conformity_processor.py
+++ b/lemmymodbot/processors/title_conformity_processor.py
@@ -1,7 +1,7 @@
 import re
 
-from processors import Processor, Content, LemmyHandle, ContentResult
-from processors.base import ContentType
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
 
 
 class TitleConformityProcessor(Processor):
diff --git a/lemmymodbot/processors/toxicity_processor.py b/lemmymodbot/processors/toxicity_processor.py
new file mode 100644
index 0000000..6c9fa92
--- /dev/null
+++ b/lemmymodbot/processors/toxicity_processor.py
@@ -0,0 +1,37 @@
+import torch
+
+from lemmymodbot.ml import ToxicityClassifier
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
+
+
+class ToxicityProcessor(Processor):
+    classifier: ToxicityClassifier
+    uncertainty_allowance = 0.2
+
+    def __init__(self, classifier: ToxicityClassifier = ToxicityClassifier()):
+        self.classifier = classifier
+
+    def setup(self) -> None:
+        self.classifier.setup()
+
+    def execute(self, content: Content, handle: LemmyHandle) -> ContentResult:
+        if content.type == ContentType.POST_LINK:
+            return ContentResult.nothing()
+
+        local_flags = []
+        preds, preds2, preds3 = self.classifier.classify(content)
+        print('\n\n' + content.content)
+        print(f'{preds}', (1 - torch.argmax(preds)).item())
+        if preds[0].item() < 0.2 and preds[1].item() < 0.2:
+            print("Low values^")
+        if abs(preds[0].item() - preds[1].item()) < self.uncertainty_allowance:
+            print("Close values^")
+        if preds2 == 1 and preds3 >= self.uncertainty_allowance:
+            print("Toxic^")
+            local_flags.append('potentially toxic')
+
+        return ContentResult(local_flags, {
+            "toxicity": preds[0].item(),
+            "non_toxicity": preds[1].item(),
+        })
diff --git a/processors/user_processor.py b/lemmymodbot/processors/user_processor.py
similarity index 79%
rename from processors/user_processor.py
rename to lemmymodbot/processors/user_processor.py
index 7b2c713..7b07bfe 100644
--- a/processors/user_processor.py
+++ b/lemmymodbot/processors/user_processor.py
@@ -1,7 +1,7 @@
 from typing import List
 
-from processors import Processor, Content, LemmyHandle, ContentResult
-from processors.base import ContentType
+from lemmymodbot.processors import Processor, Content, LemmyHandle, ContentResult
+from lemmymodbot.processors.base import ContentType
 
 
 class UserProcessor(Processor):
diff --git a/reconnection_manager.py b/lemmymodbot/reconnection_manager.py
similarity index 100%
rename from reconnection_manager.py
rename to lemmymodbot/reconnection_manager.py
diff --git a/main.py b/main.py
index aacce88..8e91dc2 100644
--- a/main.py
+++ b/main.py
@@ -1,8 +1,8 @@
-from lemmybot import LemmyBot
-from processors import ToxicityProcessor
+from lemmymodbot import LemmyBot
+from lemmymodbot import ToxicityProcessor
 
 if __name__ == "__main__":
     bot = LemmyBot([
-        ToxicityProcessor()
+        ToxicityProcessor(),
     ])
     bot.run()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..49fd1ad
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,6 @@
+[tool.ruff]
+ignore-init-module-imports = true
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F401","E402","F403"]
+"**/{tests,docs,tools}/*" = ["E402"]
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..850a1c3
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,52 @@
+-i https://pypi.org/simple
+aiostream==0.4.5
+certifi==2023.7.22 ; python_version >= '3.6'
+charset-normalizer==3.3.0 ; python_full_version >= '3.7.0'
+cmake==3.27.7
+filelock==3.12.4 ; python_version >= '3.8'
+idna==3.4 ; python_version >= '3.5'
+imagehash==4.3.1
+jinja2==3.1.2 ; python_version >= '3.7'
+joblib==1.3.2 ; python_version >= '3.7'
+lit==17.0.3
+loguru==0.7.2 ; python_version >= '3.5'
+markupsafe==2.1.3 ; python_version >= '3.7'
+matrix-client==0.4.0
+mpmath==1.3.0
+mypy-extensions==1.0.0 ; python_version >= '3.5'
+networkx==3.1 ; python_version >= '3.8'
+numpy==1.26.0 ; python_version < '3.13' and python_version >= '3.9'
+nvidia-cublas-cu11==11.10.3.66 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cuda-cupti-cu11==11.7.101 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cuda-nvrtc-cu11==11.7.99 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cuda-runtime-cu11==11.7.99 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cudnn-cu11==8.5.0.96 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cufft-cu11==10.9.0.58 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-curand-cu11==10.2.10.91 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cusolver-cu11==11.4.0.1 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-cusparse-cu11==11.7.4.91 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-nccl-cu11==2.14.3 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+nvidia-nvtx-cu11==11.7.91 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+pandas==2.1.1 ; python_version >= '3.9'
+pillow==10.1.0 ; python_version >= '3.8'
+pydantic==1.10.13 ; python_version >= '3.7'
+pylemmy==0.0.6 ; python_version >= '3.7'
+python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+pytz==2023.3.post1
+pywavelets==1.4.1 ; python_version >= '3.8'
+requests==2.31.0 ; python_version >= '3.7'
+scikit-learn==1.3.1 ; python_version >= '3.8'
+scipy==1.11.3 ; python_version < '3.13' and python_version >= '3.9'
+setuptools==68.2.2 ; python_version >= '3.8'
+six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
+sympy==1.12 ; python_version >= '3.8'
+threadpoolctl==3.2.0 ; python_version >= '3.8'
+torch==2.0.1 ; python_full_version >= '3.8.0'
+torchdata==0.6.1 ; python_version >= '3.8'
+torchtext==0.15.2 ; python_version >= '3.8'
+tqdm==4.66.1 ; python_version >= '3.7'
+triton==2.0.0 ; platform_system == 'Linux' and platform_machine == 'x86_64'
+typing-extensions==4.8.0 ; python_version >= '3.8'
+tzdata==2023.3 ; python_version >= '2'
+urllib3==1.26.18 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
+wheel==0.41.2 ; python_version >= '3.7'
diff --git a/training/train.tsv b/training/train.tsv
index 64c9506..af9ff61 100644
--- a/training/train.tsv
+++ b/training/train.tsv
@@ -1273,4 +1273,5 @@ Republican Bridge Trump Order: ◇ ♡ ♧ ♤.	"{""toxic_content"":false}"
 All the jet fuel cant melt steel beams nonsense like the fact that demolitions grade high-powered explosives being used is some conspiracy instead of the obviously necessary act to take when a skyscraper is damaged and no longer structurally sound.	"{""toxic_content"":false}"
 Do you really think making a martyr out of MLK is a strategy the US gov wouldnt have anticipated had they murdered him/let him be murdered?  Dude would not be what he is today had he not been murdered. 	"{""toxic_content"":false}"
 Trump conspired against the USA.	"{""toxic_content"":false}"
-His lawyers, some of his staff in the white house and kid rock	"{""toxic_content"":false}"
\ No newline at end of file
+His lawyers, some of his staff in the white house and kid rock	"{""toxic_content"":false}"
+What the sick fuck is this? Why has this popped up on  my page	"{""toxic_content"":true}"
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..e66fb05
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,26 @@
+name: Unit Test
+
+on:
+  push:
+    branches: [ "master", "develop" ]
+  pull_request:
+    branches: [ "master", "develop" ]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.11'
+          cache: 'pip'
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements.txt
+      - name: Test with pytest
+        run: |
+          pip install pytest pytest-cov
+          python -m pytest tests/ tests --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
diff --git a/tests/processors/test_blacklist_processor.py b/tests/processors/test_blacklist_processor.py
new file mode 100644
index 0000000..b0eba01
--- /dev/null
+++ b/tests/processors/test_blacklist_processor.py
@@ -0,0 +1,43 @@
+import unittest
+from unittest.mock import Mock
+
+from lemmymodbot import BlacklistProcessor, ContentType
+
+
+class TestBlacklistProcessor(unittest.TestCase):
+    def setUp(self):
+        self.mock_content = Mock()
+        self.mock_handle = Mock()
+
+    def test_execute_nothing_post_link(self):
+        # Test when the content type is POST_LINK, the processor should return nothing.
+        processor = BlacklistProcessor(["bad_word1", "bad_word2"])
+        processor.setup()
+
+        self.mock_content.type = ContentType.POST_LINK
+        result = processor.execute(self.mock_content, self.mock_handle)
+
+        self.assertEqual(result.flags, [])
+
+    def test_execute_no_blacklist_words(self):
+        # Test when the content doesn't contain any blacklist words, the processor should return nothing.
+        processor = BlacklistProcessor(["bad_word1", "bad_word2"])
+        processor.setup()
+
+        self.mock_content.type = ContentType.POST_TITLE  # Assuming ContentType.OTHER_TYPE is not POST_LINK
+        self.mock_content.content = "This is a normal content without bad words"
+        result = processor.execute(self.mock_content, self.mock_handle)
+
+        self.assertEqual(result.flags, [])
+
+    def test_execute_with_blacklist_words(self):
+        # Test when the content contains blacklist words, the processor should return ContentResult with a blacklist tag.
+        processor = BlacklistProcessor(["bad_word1", "bad_word2"])
+        processor.setup()
+
+        self.mock_content.type = ContentType.POST_TITLE  # Assuming ContentType.OTHER_TYPE is not POST_LINK
+        self.mock_content.content = "This contains bad_word1, which is on the blacklist"
+        result = processor.execute(self.mock_content, self.mock_handle)
+
+        self.assertEqual(result.flags, ['word_blacklist'])
+        self.mock_handle.post_comment.assert_not_called()
diff --git a/tests/processors/test_phash_processor.py b/tests/processors/test_phash_processor.py
new file mode 100644
index 0000000..963c011
--- /dev/null
+++ b/tests/processors/test_phash_processor.py
@@ -0,0 +1,99 @@
+import unittest
+from unittest.mock import Mock, patch
+
+from lemmymodbot import PhashProcessor, Content, ContentType
+
+
+class TestPhashProcessor(unittest.TestCase):
+
+    def setUp(self):
+        self.processor = PhashProcessor()
+
+    def test_execute_post_link(self):
+        content = Content(
+            community="",
+            actor_id="",
+            content="https://example.com/some-image.jpg",
+            type=ContentType.POST_LINK
+        )
+        handle = Mock()
+        handle.database.url_exists.return_value = None
+        handle.fetch_image.return_value = ("image_data", "phash_value")
+        handle.database.phash_exists.return_value = False
+        handle.database.add_phash.return_value = None
+
+        result = self.processor.execute(content, handle)
+
+        self.assertEqual(result.extras, {"phash": "phash_value"})
+        handle.post_comment.assert_not_called()
+
+    def test_execute_existing_phash(self):
+        content = Content(
+            community="",
+            actor_id="",
+            content="https://example.com/some-image.jpg",
+            type=ContentType.POST_LINK
+        )
+        handle = Mock()
+        handle.database.url_exists.return_value = None
+        handle.fetch_image.return_value = ("image_data", "phash_value")
+        handle.database.phash_exists.return_value = True
+        handle.database.add_phash.return_value = None
+        handle.database.get_post_links_by_phash.return_value = ["test"]
+
+        result = self.processor.execute(content, handle)
+
+        self.assertEqual(result.extras, {"phash": "phash_value"})
+
+    def test_execute_existing_url(self):
+        content = Content(
+            community="",
+            actor_id="",
+            content="https://example.com/some-image.jpg",
+            type=ContentType.POST_LINK
+        )
+        handle = Mock()
+        handle.database.url_exists.return_value = "phash_value"
+        handle.fetch_image.return_value = ("image_data", "phash_value")
+        handle.database.phash_exists.return_value = False
+        handle.database.add_phash.return_value = None
+        handle.database.get_post_links_by_phash.return_value = ["test"]
+
+        result = self.processor.execute(content, handle)
+
+        self.assertEqual(result.extras, {"phash": "phash_value"})
+
+    @patch('lemmymodbot.PhashProcessor._warn_user')
+    def test_execute_warning(self, mock_warn_user):
+        content = Content(
+            community="",
+            actor_id="",
+            content="https://example.com/some-image.jpg",
+            type=ContentType.POST_LINK
+        )
+        handle = Mock()
+        handle.database.url_exists.return_value = None
+        handle.fetch_image.return_value = ("image_data", "phash_value")
+        handle.database.phash_exists.return_value = True
+        handle.database.add_phash.return_value = None
+        handle.database.get_post_links_by_phash.return_value = ["test"]
+
+        self.processor.execute(content, handle)
+
+        mock_warn_user.assert_called_once_with(handle, "phash_value")
+
+    def test_warn_user(self):
+        handle = Mock()
+        handle.database.get_post_links_by_phash.return_value = ["post1", "post2"]
+
+        result = self.processor._warn_user(handle, "phash_value")
+
+        self.assertEqual(result.extras, {"phash": "phash_value"})
+        handle.post_comment.assert_called_once_with(
+            "This post appears to be a duplicate of the following posts: [link](post1), [link](post2). "
+            "This could be a false positive (beep boop I am a robot)."
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/processors/test_title_conformity_processor.py b/tests/processors/test_title_conformity_processor.py
new file mode 100644
index 0000000..4f17e5a
--- /dev/null
+++ b/tests/processors/test_title_conformity_processor.py
@@ -0,0 +1,42 @@
+import unittest
+from unittest.mock import Mock
+from lemmymodbot import TitleConformityProcessor, ContentType, Content
+
+
+class TestTitleConformityProcessor(unittest.TestCase):
+    def setUp(self):
+        self.mock_content = Mock()
+        self.mock_handle = Mock()
+
+    def test_execute_no_comment(self):
+        # Test when the content type is not POST_TITLE, no comment should be posted.
+        processor = TitleConformityProcessor("your_regex_pattern", "Your message")
+
+        processor.execute(Content(
+            "",
+            "",
+            "",
+            ContentType.POST_LINK
+        ), self.mock_handle)
+
+        self.mock_handle.post_comment.assert_not_called()
+
+    def test_execute_comment(self):
+        # Test when the content type is POST_TITLE and the pattern doesn't match, a comment should be posted.
+        processor = TitleConformityProcessor(".* \\(\\(", "Your message")
+
+        self.mock_content.type = ContentType.POST_TITLE
+        self.mock_content.content = "Title that doesn't match the pattern"
+        processor.execute(self.mock_content, self.mock_handle)
+
+        self.mock_handle.post_comment.assert_called_with("Your message")
+
+    def test_execute_nothing(self):
+        # Test when the content type is POST_TITLE and the pattern matches, no comment should be posted.
+        processor = TitleConformityProcessor(".* \\(\\(", "Your message")
+
+        self.mock_content.type = ContentType.POST_TITLE
+        self.mock_content.content = "Title that matches the pattern (("
+        processor.execute(self.mock_content, self.mock_handle)
+
+        self.mock_handle.post_comment.assert_not_called()
diff --git a/tests/processors/test_toxicity_processor.py b/tests/processors/test_toxicity_processor.py
new file mode 100644
index 0000000..ecb2695
--- /dev/null
+++ b/tests/processors/test_toxicity_processor.py
@@ -0,0 +1,66 @@
+import unittest
+from unittest.mock import Mock
+import torch
+from lemmymodbot import ToxicityProcessor, ContentType, Content
+
+
+class TestToxicityProcessor(unittest.TestCase):
+
+    def test_execute_post_link(self):
+        processor = ToxicityProcessor()
+        content = Content(
+            "",
+            "https://example.com/some-link",
+            "",
+            ContentType.POST_LINK
+        )
+        handle = Mock()
+
+        result = processor.execute(content, handle)
+
+        self.assertListEqual(result.flags, [])
+        self.assertIsNone(result.extras)
+
+    def test_execute_non_toxic_content(self):
+        classifier = Mock()
+        classifier.classify.return_value = torch.tensor([0.1, 0.9]), 0, 0
+        processor = ToxicityProcessor(classifier)
+        content = Content(
+            "",
+            "This is a non-toxic comment.",
+            "",
+            ContentType.COMMENT
+        )
+        handle = Mock()
+
+        result = processor.execute(content, handle)
+
+        self.assertListEqual(result.flags, [])
+        self.assertDictEqual(result.extras, {
+            "toxicity": 0.1,
+            "non_toxicity": 0.9,
+        })
+
+    def test_execute_potentially_toxic_content(self):
+        classifier = Mock()
+        classifier.classify.return_value = torch.tensor([0.2, 0.8]), 1, 0.3
+        processor = ToxicityProcessor(classifier)
+        content = Content(
+            "",
+            "This is a potentially toxic comment.",
+            "",
+            ContentType.COMMENT
+        )
+        handle = Mock()
+
+        result = processor.execute(content, handle)
+
+        self.assertListEqual(result.flags, ['potentially toxic'])
+        self.assertDictEqual(result.extras, {
+            "toxicity": 0.2,
+            "non_toxicity": 0.8,
+        })
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/processors/test_user_processor.py b/tests/processors/test_user_processor.py
new file mode 100644
index 0000000..43d2540
--- /dev/null
+++ b/tests/processors/test_user_processor.py
@@ -0,0 +1,55 @@
+import unittest
+from unittest.mock import Mock
+
+from lemmymodbot import UserProcessor, Content, ContentType
+
+
+class TestUserWatchListProcessor(unittest.TestCase):
+
+    def test_execute_post_link(self):
+        user_watch_list = UserProcessor(["user1", "user2"])
+        content = Content(
+            "",
+            "https://example.com/some-link",
+            "",
+            ContentType.POST_LINK
+        )
+        handle = Mock()
+
+        result = user_watch_list.execute(content, handle)
+
+        self.assertListEqual(result.flags, [])
+        self.assertIsNone(result.extras)
+
+    def test_execute_user_in_watch_list(self):
+        user_watch_list = UserProcessor(["user1", "user2"])
+        content = Content(
+            "",
+            "https://example.com/some-content",
+            "user2",
+            ContentType.COMMENT,
+        )
+        handle = Mock()
+
+        result = user_watch_list.execute(content, handle)
+
+        self.assertListEqual(result.flags, ['user_watch_list'])
+        self.assertIsNone(result.extras)
+
+    def test_execute_user_not_in_watch_list(self):
+        user_watch_list = UserProcessor(["user1", "user2"])
+        content = Content(
+            "",
+            "https://example.com/some-content",
+            "user3",
+            ContentType.COMMENT
+        )
+        handle = Mock()
+
+        result = user_watch_list.execute(content, handle)
+
+        self.assertListEqual(result.flags, [])
+        self.assertIsNone(result.extras)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test.py b/tests/test.py
new file mode 100644
index 0000000..f636d3f
--- /dev/null
+++ b/tests/test.py
@@ -0,0 +1,4 @@
+import unittest
+
+if __name__ == '__main__':
+    unittest.main()