mirror of
				https://github.com/TheLovinator1/discord-reminder-bot.git
				synced 2025-10-31 00:29:49 +01:00 
			
		
		
		
	Send errors and missed reminders to webhook
New environment variable that you need to use for this to work.
This commit is contained in:
		
							
								
								
									
										19
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								.env.example
									
									
									
									
									
								
							| @@ -1,15 +1,22 @@ | |||||||
| # Discord bot token | # Discord bot token | ||||||
|  | # https://discord.com/developers/applications | ||||||
| BOT_TOKEN=JFIiasfjioFIAOJFOIJIOSAF.AFo-7A.akwFakeopfaWPOKawPOFKOAKFPA | BOT_TOKEN=JFIiasfjioFIAOJFOIJIOSAF.AFo-7A.akwFakeopfaWPOKawPOFKOAKFPA | ||||||
|  |  | ||||||
| # Timezone | # Time zone that you are in. This is used when typing "Friday 22:00" and more. | ||||||
| # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List | # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List | ||||||
| # You want to use the TZ databse name. | # You want to use the TZ databse name. | ||||||
| TIMEZONE=Europe/Stockholm | TIMEZONE=Europe/Stockholm | ||||||
|  |  | ||||||
| # Optional: Change sqlite database location | # Optional: Change sqlite database location. | ||||||
| # SQLITE_LOCATION=/jobs.sqlite # This will be created in the repo root (This is the default if not set) | # SQLITE_LOCATION=/jobs.sqlite # This will be created in the repo root (This is the default if not set). | ||||||
| # SQLITE_LOCATION=/C:\\Users\\Jocke\\Desktop\\db.sqlite3 # Note the double backslashes and the first slash | # SQLITE_LOCATION=/C:\\Users\\Jocke\\Desktop\\db.sqlite3 # Note the double backslashes and the first slash. | ||||||
| # SQLITE_LOCATION=//home/lovinator/foo.db # On Linux you will need to use double slashes before the path to get the absolute path | # SQLITE_LOCATION=//home/lovinator/foo.db # On Linux you will need to use double slashes before the path to get the | ||||||
|  | # absolute path. | ||||||
|  |  | ||||||
| # Log level, CRITICAL, ERROR, WARNING, INFO, DEBUG | # Log level, CRITICAL, ERROR, WARNING, INFO, DEBUG. | ||||||
| LOG_LEVEL=INFO | LOG_LEVEL=INFO | ||||||
|  |  | ||||||
|  | # Webhook that discord-reminder-bot will send errors and information about missed reminders. | ||||||
|  | # https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks | ||||||
|  | # Right click channel in Discord -> Intergrations -> Webhooks -> Create Webhook. | ||||||
|  | WEBHOOK_URL=https://discord.com/api/webhooks/582696524044304394/a3CMwZWchmHAXItB_lzSSRYBx0-AlPAHseJWqhHLfsAg_X4erac9-CeVeUDqPI1ac1vT | ||||||
| @@ -5,9 +5,12 @@ from typing import List | |||||||
|  |  | ||||||
| import dateparser | import dateparser | ||||||
| import interactions | import interactions | ||||||
|  | from apscheduler import events | ||||||
|  | from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_MISSED | ||||||
| from apscheduler.jobstores.base import JobLookupError | from apscheduler.jobstores.base import JobLookupError | ||||||
| from apscheduler.triggers.date import DateTrigger | from apscheduler.triggers.date import DateTrigger | ||||||
| from dateparser.conf import SettingValidationError | from dateparser.conf import SettingValidationError | ||||||
|  | from discord_webhook import DiscordWebhook | ||||||
| from interactions import CommandContext, Embed, Option, OptionType, autodefer | from interactions import CommandContext, Embed, Option, OptionType, autodefer | ||||||
| from interactions.ext.paginator import Paginator | from interactions.ext.paginator import Paginator | ||||||
|  |  | ||||||
| @@ -19,11 +22,27 @@ from discord_reminder_bot.settings import ( | |||||||
|     log_level, |     log_level, | ||||||
|     scheduler, |     scheduler, | ||||||
|     sqlite_location, |     sqlite_location, | ||||||
|  |     webhook_url | ||||||
| ) | ) | ||||||
|  |  | ||||||
| bot = interactions.Client(token=bot_token) | bot = interactions.Client(token=bot_token) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def send_webhook(url=webhook_url, message: str = "discord-reminder-bot: Empty message."): | ||||||
|  |     """ | ||||||
|  |     Send a webhook to Discord. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         url: Our webhook url, defaults to the one from settings. | ||||||
|  |         message: The message that will be sent to Discord. | ||||||
|  |     """ | ||||||
|  |     if not url: | ||||||
|  |         print("ERROR: Tried to send a webhook but you have no webhook url configured.") | ||||||
|  |         return | ||||||
|  |     webhook = DiscordWebhook(url=url, content=message, rate_limit_retry=True) | ||||||
|  |     webhook.execute() | ||||||
|  |  | ||||||
|  |  | ||||||
| @bot.command(name="remind") | @bot.command(name="remind") | ||||||
| async def base_command(ctx: interactions.CommandContext): | async def base_command(ctx: interactions.CommandContext): | ||||||
|     """This description isn't seen in the UI (yet?) |     """This description isn't seen in the UI (yet?) | ||||||
| @@ -639,6 +658,19 @@ async def remind_interval( | |||||||
|     await ctx.send(message) |     await ctx.send(message) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def my_listener(event): | ||||||
|  |     """This gets called when something in APScheduler happens.""" | ||||||
|  |     if event.code == events.EVENT_JOB_MISSED: | ||||||
|  |         # TODO: Is it possible to get the message? | ||||||
|  |         scheduled_time = event.scheduled_run_time.strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |         msg = f"Job {event.job_id} was missed! Was scheduled at {scheduled_time}" | ||||||
|  |         send_webhook(message=msg) | ||||||
|  |  | ||||||
|  |     if event.exception: | ||||||
|  |         send_webhook(f"discord-reminder-bot failed to send message to Discord\n" | ||||||
|  |                      f"{event}") | ||||||
|  |  | ||||||
|  |  | ||||||
| async def send_to_discord(channel_id: int, message: str, author_id: int): | async def send_to_discord(channel_id: int, message: str, author_id: int): | ||||||
|     """Send a message to Discord. |     """Send a message to Discord. | ||||||
|  |  | ||||||
| @@ -647,8 +679,7 @@ async def send_to_discord(channel_id: int, message: str, author_id: int): | |||||||
|         message: The message. |         message: The message. | ||||||
|         author_id: User we should ping. |         author_id: User we should ping. | ||||||
|     """ |     """ | ||||||
|     # TODO: Check if channel exists. |  | ||||||
|     # TODO: Send message to webhook if channel is not found. |  | ||||||
|     channel = await interactions.get( |     channel = await interactions.get( | ||||||
|         bot, |         bot, | ||||||
|         interactions.Channel, |         interactions.Channel, | ||||||
| @@ -669,8 +700,8 @@ def start(): | |||||||
|         f"config_timezone = {config_timezone}\n" |         f"config_timezone = {config_timezone}\n" | ||||||
|         f"log_level = {log_level}" |         f"log_level = {log_level}" | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     scheduler.start() |     scheduler.start() | ||||||
|  |     scheduler.add_listener(my_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR) | ||||||
|     bot.start() |     bot.start() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,8 @@ load_dotenv(verbose=True) | |||||||
| sqlite_location = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite") | sqlite_location = os.getenv("SQLITE_LOCATION", default="/jobs.sqlite") | ||||||
| config_timezone = os.getenv("TIMEZONE", default="UTC") | config_timezone = os.getenv("TIMEZONE", default="UTC") | ||||||
| bot_token = os.getenv("BOT_TOKEN", default="") | bot_token = os.getenv("BOT_TOKEN", default="") | ||||||
| log_level = os.getenv(key="LOG_LEVEL", default="INFO") | log_level = os.getenv("LOG_LEVEL", default="INFO") | ||||||
|  | webhook_url = os.getenv("WEBHOOK_URL", default="") | ||||||
|  |  | ||||||
| if not bot_token: | if not bot_token: | ||||||
|     raise ValueError("Missing bot token") |     raise ValueError("Missing bot token") | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										73
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @@ -78,6 +78,14 @@ docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] | |||||||
| tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] | ||||||
| tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] | tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "certifi" | ||||||
|  | version = "2022.9.24" | ||||||
|  | description = "Python package for providing Mozilla's CA Bundle." | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "charset-normalizer" | name = "charset-normalizer" | ||||||
| version = "2.1.1" | version = "2.1.1" | ||||||
| @@ -157,7 +165,21 @@ readthedocs = ["Sphinx", "enum-tools[sphinx]", "furo", "sphinx-copybutton", "sph | |||||||
| type = "git" | type = "git" | ||||||
| url = "https://github.com/interactions-py/library.git" | url = "https://github.com/interactions-py/library.git" | ||||||
| reference = "unstable" | reference = "unstable" | ||||||
| resolved_reference = "4cd4538f6d90b0b31471d7e5dcb98d68fc6d7987" | resolved_reference = "596b95f8a97194920dfdb70a55b52493ff63554f" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "discord-webhook" | ||||||
|  | version = "0.17.0" | ||||||
|  | description = "execute discord webhooks" | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | requests = ">=2.19.1" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | async = ["httpx (>=0.20.0)"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "exceptiongroup" | name = "exceptiongroup" | ||||||
| @@ -327,6 +349,24 @@ category = "main" | |||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.6" | python-versions = ">=3.6" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "requests" | ||||||
|  | version = "2.28.1" | ||||||
|  | description = "Python HTTP for Humans." | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7, <4" | ||||||
|  |  | ||||||
|  | [package.dependencies] | ||||||
|  | certifi = ">=2017.4.17" | ||||||
|  | charset-normalizer = ">=2,<3" | ||||||
|  | idna = ">=2.5,<4" | ||||||
|  | urllib3 = ">=1.21.1,<1.27" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | socks = ["PySocks (>=1.5.6,!=1.5.7)"] | ||||||
|  | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "setuptools" | name = "setuptools" | ||||||
| version = "65.5.0" | version = "65.5.0" | ||||||
| @@ -412,6 +452,19 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} | |||||||
| devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] | devenv = ["black", "pyroma", "pytest-cov", "zest.releaser"] | ||||||
| test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"] | test = ["pytest (>=4.3)", "pytest-mock (>=3.3)"] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "urllib3" | ||||||
|  | version = "1.26.12" | ||||||
|  | description = "HTTP library with thread-safe connection pooling, file post, and more." | ||||||
|  | category = "main" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" | ||||||
|  |  | ||||||
|  | [package.extras] | ||||||
|  | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] | ||||||
|  | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] | ||||||
|  | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "yarl" | name = "yarl" | ||||||
| version = "1.8.1" | version = "1.8.1" | ||||||
| @@ -427,7 +480,7 @@ multidict = ">=4.0" | |||||||
| [metadata] | [metadata] | ||||||
| lock-version = "1.1" | lock-version = "1.1" | ||||||
| python-versions = "^3.9" | python-versions = "^3.9" | ||||||
| content-hash = "c64bd02df0b561448367fecf47b590289f6c3f91cf6e0524e10f3d858854d8cc" | content-hash = "d6a5cb22ff5db1181c1a877bf8bf13852d2f724dba691fc2ae26161b0ddbd8ab" | ||||||
|  |  | ||||||
| [metadata.files] | [metadata.files] | ||||||
| aiohttp = [ | aiohttp = [ | ||||||
| @@ -535,6 +588,10 @@ attrs = [ | |||||||
|     {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, |     {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, | ||||||
|     {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, |     {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, | ||||||
| ] | ] | ||||||
|  | certifi = [ | ||||||
|  |     {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, | ||||||
|  |     {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, | ||||||
|  | ] | ||||||
| charset-normalizer = [ | charset-normalizer = [ | ||||||
|     {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, |     {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, | ||||||
|     {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, |     {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, | ||||||
| @@ -549,6 +606,10 @@ dateparser = [ | |||||||
| ] | ] | ||||||
| dinteractions-Paginator = [] | dinteractions-Paginator = [] | ||||||
| discord-py-interactions = [] | discord-py-interactions = [] | ||||||
|  | discord-webhook = [ | ||||||
|  |     {file = "discord-webhook-0.17.0.tar.gz", hash = "sha256:bb47e5fc83f73614d7b2a3764b84359b52c96a94aadf3302bc3c067dd21b43cc"}, | ||||||
|  |     {file = "discord_webhook-0.17.0-py3-none-any.whl", hash = "sha256:d869849c4834f928f5c22597dc7600b1a30f9e797d1aeb1d4196711a243d9a73"}, | ||||||
|  | ] | ||||||
| exceptiongroup = [ | exceptiongroup = [ | ||||||
|     {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, |     {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, | ||||||
|     {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, |     {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, | ||||||
| @@ -863,6 +924,10 @@ regex = [ | |||||||
|     {file = "regex-2022.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9efa41d1527b366c88f265a227b20bcec65bda879962e3fc8a2aee11e81266d7"}, |     {file = "regex-2022.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9efa41d1527b366c88f265a227b20bcec65bda879962e3fc8a2aee11e81266d7"}, | ||||||
|     {file = "regex-2022.3.2.tar.gz", hash = "sha256:79e5af1ff258bc0fe0bdd6f69bc4ae33935a898e3cbefbbccf22e88a27fa053b"}, |     {file = "regex-2022.3.2.tar.gz", hash = "sha256:79e5af1ff258bc0fe0bdd6f69bc4ae33935a898e3cbefbbccf22e88a27fa053b"}, | ||||||
| ] | ] | ||||||
|  | requests = [ | ||||||
|  |     {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, | ||||||
|  |     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, | ||||||
|  | ] | ||||||
| setuptools = [ | setuptools = [ | ||||||
|     {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, |     {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, | ||||||
|     {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, |     {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, | ||||||
| @@ -926,6 +991,10 @@ tzlocal = [ | |||||||
|     {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, |     {file = "tzlocal-4.2-py3-none-any.whl", hash = "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745"}, | ||||||
|     {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, |     {file = "tzlocal-4.2.tar.gz", hash = "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"}, | ||||||
| ] | ] | ||||||
|  | urllib3 = [ | ||||||
|  |     {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, | ||||||
|  |     {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, | ||||||
|  | ] | ||||||
| yarl = [ | yarl = [ | ||||||
|     {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, |     {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, | ||||||
|     {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, |     {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ sqlalchemy = "^1.4.42" | |||||||
| discord-py-interactions = { git = "https://github.com/interactions-py/library.git", rev = "unstable" } | discord-py-interactions = { git = "https://github.com/interactions-py/library.git", rev = "unstable" } | ||||||
| interactions-wait-for = "^1.0.6" | interactions-wait-for = "^1.0.6" | ||||||
| dinteractions-paginator = { git = "https://github.com/interactions-py/paginator.git", rev = "unstable" } | dinteractions-paginator = { git = "https://github.com/interactions-py/paginator.git", rev = "unstable" } | ||||||
|  | discord-webhook = "^0.17.0" | ||||||
|  |  | ||||||
| [tool.poetry.dev-dependencies] | [tool.poetry.dev-dependencies] | ||||||
| pytest = "^7.1.2" | pytest = "^7.1.2" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user