Start refactoring bot, add /remind add
command
This commit is contained in:
@ -19,4 +19,4 @@ LOG_LEVEL=INFO
|
|||||||
# Webhook that discord-reminder-bot will send errors and information about missed reminders.
|
# 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
|
# https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
|
||||||
# Right click channel in Discord -> Intergrations -> Webhooks -> Create Webhook.
|
# Right click channel in Discord -> Intergrations -> Webhooks -> Create Webhook.
|
||||||
WEBHOOK_URL=https://discord.com/api/webhooks/582696524044304394/a3CMwZWchmHAXItB_lzSSRYBx0-AlPAHseJWqhHLfsAg_X4erac9-CeVeUDqPI1ac1vT
|
WEBHOOK_URL=https://discord.com/api/webhooks/582696524044304394/a3CMwZWchmHAXItB_lzSSRYBx0-AlPAHseJWqhHLfsAg_X4erac9-CeVeUDqPI1ac1vT
|
||||||
|
@ -38,7 +38,7 @@ repos:
|
|||||||
|
|
||||||
# An extremely fast Python linter and formatter.
|
# An extremely fast Python linter and formatter.
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.8.4
|
rev: v0.8.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python Debugger: Start bot",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/discord_reminder_bot/main.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
20
.vscode/settings.json
vendored
Normal file
20
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"apscheduler",
|
||||||
|
"asctime",
|
||||||
|
"audioop",
|
||||||
|
"dateparser",
|
||||||
|
"docstrings",
|
||||||
|
"dotenv",
|
||||||
|
"hikari",
|
||||||
|
"isort",
|
||||||
|
"jobstores",
|
||||||
|
"levelname",
|
||||||
|
"Lovinator",
|
||||||
|
"pycodestyle",
|
||||||
|
"pydocstyle",
|
||||||
|
"sqlalchemy",
|
||||||
|
"uvloop"
|
||||||
|
],
|
||||||
|
"python.analysis.typeCheckingMode": "standard"
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.triggers.date import DateTrigger
|
|
||||||
|
|
||||||
from discord_reminder_bot.settings import config_timezone
|
|
||||||
|
|
||||||
|
|
||||||
def calculate(job: Job) -> str:
|
|
||||||
"""Get trigger time from a reminder and calculate how many days, hours and minutes till trigger.
|
|
||||||
|
|
||||||
Days/Minutes will not be included if 0.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job. Can be cron, interval or normal.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Returns days, hours and minutes till the reminder. Returns "Couldn't calculate time" if no job is found.
|
|
||||||
"""
|
|
||||||
# TODO: This "breaks" when only seconds are left.
|
|
||||||
# If we use (in {calc_countdown(job)}) it will show (in )
|
|
||||||
|
|
||||||
trigger_time: datetime | None = job.trigger.run_date if type(job.trigger) is DateTrigger else job.next_run_time
|
|
||||||
|
|
||||||
# Get_job() returns None when it can't find a job with that ID.
|
|
||||||
if trigger_time is None:
|
|
||||||
# TODO: Change this to None and send this text where needed.
|
|
||||||
return "Couldn't calculate time"
|
|
||||||
|
|
||||||
# Get time and date the job will run and calculate how many days,
|
|
||||||
# hours and seconds.
|
|
||||||
return countdown(trigger_time)
|
|
||||||
|
|
||||||
|
|
||||||
def countdown(trigger_time: datetime) -> str:
|
|
||||||
"""Calculate days, hours and minutes to a date.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
trigger_time: The date.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A string with the days, hours and minutes.
|
|
||||||
"""
|
|
||||||
countdown_time: timedelta = trigger_time - datetime.now(tz=pytz.timezone(config_timezone))
|
|
||||||
|
|
||||||
days, hours, minutes = (
|
|
||||||
countdown_time.days,
|
|
||||||
countdown_time.seconds // 3600,
|
|
||||||
countdown_time.seconds // 60 % 60,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return seconds if only seconds are left.
|
|
||||||
if days == 0 and hours == 0 and minutes == 0:
|
|
||||||
seconds: int = countdown_time.seconds % 60
|
|
||||||
return f"{seconds} second" + ("s" if seconds != 1 else "")
|
|
||||||
|
|
||||||
# TODO: Explain this.
|
|
||||||
return ", ".join(
|
|
||||||
f"{x} {y}{'s' * (x != 1)}"
|
|
||||||
for x, y in (
|
|
||||||
(days, "day"),
|
|
||||||
(hours, "hour"),
|
|
||||||
(minutes, "minute"),
|
|
||||||
)
|
|
||||||
if x
|
|
||||||
)
|
|
@ -1,311 +0,0 @@
|
|||||||
from collections.abc import Generator
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
|
||||||
|
|
||||||
import interactions
|
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.schedulers.base import BaseScheduler
|
|
||||||
from apscheduler.triggers.date import DateTrigger
|
|
||||||
from interactions import (
|
|
||||||
ActionRow,
|
|
||||||
Button,
|
|
||||||
ButtonStyle,
|
|
||||||
Channel,
|
|
||||||
CommandContext,
|
|
||||||
ComponentContext,
|
|
||||||
Embed,
|
|
||||||
Message,
|
|
||||||
Modal,
|
|
||||||
TextInput,
|
|
||||||
)
|
|
||||||
from interactions.ext.paginator import Page, Paginator, RowPosition
|
|
||||||
|
|
||||||
from discord_reminder_bot.countdown import calculate
|
|
||||||
from discord_reminder_bot.settings import scheduler
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
max_message_length: Literal[1010] = 1010
|
|
||||||
max_title_length: Literal[90] = 90
|
|
||||||
|
|
||||||
|
|
||||||
def _get_trigger_text(job: Job) -> str:
|
|
||||||
"""Get trigger time from a reminder and calculate how many days, hours and minutes till trigger.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job. Can be cron, interval or normal.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The trigger time and countdown till trigger. If the job is paused, it will return "_Paused_".
|
|
||||||
"""
|
|
||||||
# TODO: Add support for cron jobs and interval jobs
|
|
||||||
trigger_time: datetime | None = job.trigger.run_date if type(job.trigger) is DateTrigger else job.next_run_time
|
|
||||||
return "_Paused_" if trigger_time is None else f'{trigger_time.strftime("%Y-%m-%d %H:%M")} (in {calculate(job)})'
|
|
||||||
|
|
||||||
|
|
||||||
def _make_button(label: str, style: ButtonStyle) -> Button:
|
|
||||||
"""Make a button.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
label: The label of the button.
|
|
||||||
style: The style of the button.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Button: The button.
|
|
||||||
"""
|
|
||||||
return interactions.Button(
|
|
||||||
label=label,
|
|
||||||
style=style,
|
|
||||||
custom_id=label.lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pause_or_unpause_button(job: Job) -> Button | None:
|
|
||||||
"""Get pause or unpause button.
|
|
||||||
|
|
||||||
If the job is paused, it will return the unpause button.
|
|
||||||
If the job is not paused, it will return the pause button.
|
|
||||||
If the job is not a cron or interval job, it will return None.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job. Can be cron, interval or normal.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Button | None: The pause or unpause button. If the job is not a cron or interval job, it will return None.
|
|
||||||
"""
|
|
||||||
if type(job.trigger) is not DateTrigger:
|
|
||||||
pause_button: Button = _make_button("Pause", interactions.ButtonStyle.PRIMARY)
|
|
||||||
unpause_button: Button = _make_button("Unpause", interactions.ButtonStyle.PRIMARY)
|
|
||||||
|
|
||||||
if not hasattr(job, "next_run_time"):
|
|
||||||
return pause_button
|
|
||||||
|
|
||||||
return unpause_button if job.next_run_time is None else pause_button
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_row_of_buttons(job: Job) -> ActionRow:
|
|
||||||
"""Get components(buttons) for a page in /reminder list.
|
|
||||||
|
|
||||||
These buttons are below the embed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job. Can be cron, interval or normal.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ActionRow: A row of buttons.
|
|
||||||
"""
|
|
||||||
components: list[Button] = [
|
|
||||||
_make_button("Edit", interactions.ButtonStyle.PRIMARY),
|
|
||||||
_make_button("Remove", interactions.ButtonStyle.DANGER),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add pause/unpause button as the second button if it's a cron or interval job
|
|
||||||
pause_or_unpause_button: Button | None = _get_pause_or_unpause_button(job=job)
|
|
||||||
if pause_or_unpause_button is not None:
|
|
||||||
components.insert(1, pause_or_unpause_button)
|
|
||||||
|
|
||||||
# TODO: Should fix the type error
|
|
||||||
return ActionRow(components=components) # type: ignore # noqa: PGH003
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pages(job: Job, channel: Channel, ctx: CommandContext) -> Generator[Page, None, None]:
|
|
||||||
"""Get pages for a reminder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job. Can be cron, interval or normal.
|
|
||||||
channel: Check if the job kwargs channel ID is the same as the channel ID we looped through.
|
|
||||||
ctx: The context. Used to get the guild ID.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Generator[Page, None, None]: A page.
|
|
||||||
"""
|
|
||||||
# Get channel ID and guild ID from job kwargs
|
|
||||||
channel_id: int = job.kwargs.get("channel_id")
|
|
||||||
guild_id: int = job.kwargs.get("guild_id")
|
|
||||||
|
|
||||||
if int(channel.id) == channel_id or ctx.guild_id == guild_id:
|
|
||||||
message: str = job.kwargs.get("message")
|
|
||||||
|
|
||||||
# If message is longer than 1000 characters, truncate it
|
|
||||||
message = f"{message[:1000]}..." if len(message) > max_message_length else message
|
|
||||||
|
|
||||||
# Create embed for the singular page
|
|
||||||
embed: Embed = interactions.Embed(
|
|
||||||
title=f"{job.id}", # Example: 593dcc18aab748faa571017454669eae
|
|
||||||
fields=[
|
|
||||||
interactions.EmbedField(
|
|
||||||
name="**Channel:**",
|
|
||||||
value=f"#{channel.name}", # Example: #general
|
|
||||||
),
|
|
||||||
interactions.EmbedField(
|
|
||||||
name="**Message:**",
|
|
||||||
value=f"{message}", # Example: Don't forget to feed the cat!
|
|
||||||
),
|
|
||||||
interactions.EmbedField(
|
|
||||||
name="**Trigger:**",
|
|
||||||
value=_get_trigger_text(job=job), # Example: 2023-08-24 00:06 (in 157 days, 23 hours, 49 minutes)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Truncate title if it's longer than 90 characters
|
|
||||||
# This is the text that shows up in the dropdown menu
|
|
||||||
# Example: 2: Don't forget to feed the cat!
|
|
||||||
dropdown_title: str = f"{message[:87]}..." if len(message) > max_title_length else message
|
|
||||||
|
|
||||||
# Create a page and return it
|
|
||||||
yield Page(
|
|
||||||
embeds=embed,
|
|
||||||
title=dropdown_title,
|
|
||||||
components=_get_row_of_buttons(job),
|
|
||||||
callback=_callback,
|
|
||||||
position=RowPosition.BOTTOM,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_job(job: Job) -> str:
|
|
||||||
"""Remove a job.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job to remove.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The message to send to Discord.
|
|
||||||
"""
|
|
||||||
# TODO: Check if job exists before removing it?
|
|
||||||
# TODO: Add button to undo the removal?
|
|
||||||
channel_id: int = job.kwargs.get("channel_id")
|
|
||||||
old_message: str = job.kwargs.get("message")
|
|
||||||
try:
|
|
||||||
trigger_time: datetime | str = job.trigger.run_date
|
|
||||||
except AttributeError:
|
|
||||||
trigger_time = "N/A"
|
|
||||||
scheduler.remove_job(job.id)
|
|
||||||
|
|
||||||
return f"Job {job.id} removed.\n**Message:** {old_message}\n**Channel:** {channel_id}\n**Time:** {trigger_time}"
|
|
||||||
|
|
||||||
|
|
||||||
def _unpause_job(job: Job, custom_scheduler: BaseScheduler = scheduler) -> str:
|
|
||||||
"""Unpause a job.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job to unpause.
|
|
||||||
custom_scheduler: The scheduler to use. Defaults to the global scheduler.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The message to send to Discord.
|
|
||||||
"""
|
|
||||||
# TODO: Should we check if the job is paused before unpause it?
|
|
||||||
custom_scheduler.resume_job(job.id)
|
|
||||||
return f"Job {job.id} unpaused."
|
|
||||||
|
|
||||||
|
|
||||||
def _pause_job(job: Job, custom_scheduler: BaseScheduler = scheduler) -> str:
|
|
||||||
"""Pause a job.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
job: The job to pause.
|
|
||||||
custom_scheduler: The scheduler to use. Defaults to the global scheduler.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The message to send to Discord.
|
|
||||||
"""
|
|
||||||
# TODO: Should we check if the job is unpaused before unpause it?
|
|
||||||
custom_scheduler.pause_job(job.id)
|
|
||||||
return f"Job {job.id} paused."
|
|
||||||
|
|
||||||
|
|
||||||
async def _callback(self: Paginator, ctx: ComponentContext) -> Message | None:
|
|
||||||
"""Callback for the paginator."""
|
|
||||||
# TODO: Create a test for this
|
|
||||||
if self.component_ctx is None or self.component_ctx.message is None:
|
|
||||||
return await ctx.send("Something went wrong.", ephemeral=True)
|
|
||||||
|
|
||||||
job_id: str | None = self.component_ctx.message.embeds[0].title
|
|
||||||
job: Job | None = scheduler.get_job(job_id)
|
|
||||||
|
|
||||||
if job is None:
|
|
||||||
return await ctx.send("Job not found.", ephemeral=True)
|
|
||||||
|
|
||||||
job.kwargs.get("channel_id")
|
|
||||||
old_message: str = job.kwargs.get("message")
|
|
||||||
|
|
||||||
components: list[TextInput] = [
|
|
||||||
interactions.TextInput(
|
|
||||||
style=interactions.TextStyleType.PARAGRAPH,
|
|
||||||
label="New message",
|
|
||||||
custom_id="new_message",
|
|
||||||
value=old_message,
|
|
||||||
required=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
job_type = "cron/interval"
|
|
||||||
if type(job.trigger) is DateTrigger:
|
|
||||||
# Get trigger time for normal reminders
|
|
||||||
trigger_time: datetime | None = job.trigger.run_date
|
|
||||||
job_type: str = "normal"
|
|
||||||
components.append(
|
|
||||||
interactions.TextInput(
|
|
||||||
style=interactions.TextStyleType.SHORT,
|
|
||||||
label="New date, Can be human readable or ISO8601",
|
|
||||||
custom_id="new_date",
|
|
||||||
value=str(trigger_time),
|
|
||||||
required=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check what button was clicked and call the correct function
|
|
||||||
msg = "Something went wrong. I don't know what you clicked."
|
|
||||||
if ctx.custom_id == "edit":
|
|
||||||
# TODO: Add buttons to increase/decrease hour
|
|
||||||
modal: Modal = interactions.Modal(
|
|
||||||
title=f"Edit {job_type} reminder.",
|
|
||||||
custom_id="edit_modal",
|
|
||||||
components=components, # type: ignore # noqa: PGH003
|
|
||||||
)
|
|
||||||
await ctx.popup(modal)
|
|
||||||
msg = f"You modified {job_id}"
|
|
||||||
elif ctx.custom_id == "pause":
|
|
||||||
msg: str = _pause_job(job)
|
|
||||||
elif ctx.custom_id == "unpause":
|
|
||||||
msg: str = _unpause_job(job)
|
|
||||||
elif ctx.custom_id == "remove":
|
|
||||||
msg: str = _remove_job(job)
|
|
||||||
|
|
||||||
return await ctx.send(msg, ephemeral=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_pages(ctx: CommandContext) -> list[Page]:
|
|
||||||
"""Create pages for the paginator.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ctx: The context of the command.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Page]: A list of pages.
|
|
||||||
"""
|
|
||||||
# TODO: Add tests for this
|
|
||||||
pages: list[Page] = []
|
|
||||||
|
|
||||||
jobs: list[Job] = scheduler.get_jobs()
|
|
||||||
for job in jobs:
|
|
||||||
# Check if we're in a server
|
|
||||||
if ctx.guild is None:
|
|
||||||
await ctx.send("I can't find the server you're in. Are you sure you're in a server?", ephemeral=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Check if we're in a channel
|
|
||||||
if ctx.guild.channels is None:
|
|
||||||
await ctx.send("I can't find the channel you're in.", ephemeral=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Only add reminders from channels in the server we run "/reminder list" in
|
|
||||||
# Check if channel is in the Discord server, if not, skip it.
|
|
||||||
for channel in ctx.guild.channels:
|
|
||||||
# Add a page for each reminder
|
|
||||||
pages.extend(iter(_get_pages(job=job, channel=channel, ctx=ctx)))
|
|
||||||
return pages
|
|
File diff suppressed because it is too large
Load Diff
@ -1,70 +0,0 @@
|
|||||||
import dataclasses
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import dateparser
|
|
||||||
from dateparser.conf import SettingValidationError
|
|
||||||
|
|
||||||
from discord_reminder_bot.settings import config_timezone
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class ParsedTime:
|
|
||||||
"""This is used when parsing a time or date from a string.
|
|
||||||
|
|
||||||
We use this when adding a job with /reminder add.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
date_to_parse: The string we parsed the time from.
|
|
||||||
err: True if an error was raised when parsing the time.
|
|
||||||
err_msg: The error message.
|
|
||||||
parsed_time: The parsed time we got from the string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
date_to_parse: str | None = None
|
|
||||||
err: bool = False
|
|
||||||
err_msg: str = ""
|
|
||||||
parsed_time: datetime | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_time(date_to_parse: str, timezone: str = config_timezone) -> ParsedTime:
|
|
||||||
"""Parse the datetime from a string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
date_to_parse: The string we want to parse.
|
|
||||||
timezone: The timezone to use when parsing. This will be used when typing things like "22:00".
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ParsedTime
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
parsed_date: datetime | None = dateparser.parse(
|
|
||||||
f"{date_to_parse}",
|
|
||||||
settings={
|
|
||||||
"PREFER_DATES_FROM": "future",
|
|
||||||
"TIMEZONE": f"{timezone}",
|
|
||||||
"TO_TIMEZONE": f"{timezone}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except SettingValidationError as e:
|
|
||||||
return ParsedTime(
|
|
||||||
err=True,
|
|
||||||
err_msg=f"Timezone is possible wrong?: {e}",
|
|
||||||
date_to_parse=date_to_parse,
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
return ParsedTime(
|
|
||||||
err=True,
|
|
||||||
err_msg=f"Failed to parse date. Unknown language: {e}",
|
|
||||||
date_to_parse=date_to_parse,
|
|
||||||
)
|
|
||||||
except TypeError as e:
|
|
||||||
return ParsedTime(err=True, err_msg=f"{e}", date_to_parse=date_to_parse)
|
|
||||||
return (
|
|
||||||
ParsedTime(parsed_time=parsed_date, date_to_parse=date_to_parse)
|
|
||||||
if parsed_date
|
|
||||||
else ParsedTime(
|
|
||||||
err=True,
|
|
||||||
err_msg="Could not parse the date.",
|
|
||||||
date_to_parse=date_to_parse,
|
|
||||||
)
|
|
||||||
)
|
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
0
interactions/__init__.py
Normal file
0
interactions/__init__.py
Normal file
0
interactions/api/__init__.py
Normal file
0
interactions/api/__init__.py
Normal file
0
interactions/api/models/__init__.py
Normal file
0
interactions/api/models/__init__.py
Normal file
25
interactions/api/models/misc.py
Normal file
25
interactions/api/models/misc.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""This file is only here so we can unpickle the old jobs."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class Snowflake:
|
||||||
|
"""A class to represent a Discord snowflake."""
|
||||||
|
|
||||||
|
__slots__: list[str] = ["_snowflake"]
|
||||||
|
|
||||||
|
def __init__(self, snowflake: int | str | Snowflake) -> None:
|
||||||
|
"""Initialize the Snowflake object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
snowflake (int | str | Snowflake): The snowflake to store.
|
||||||
|
"""
|
||||||
|
self._snowflake = str(snowflake)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return the snowflake as a string."""
|
||||||
|
return self._snowflake
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
"""Return the snowflake as an integer."""
|
||||||
|
return int(self._snowflake)
|
1133
poetry.lock
generated
1133
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ dependencies = [
|
|||||||
"apscheduler<4.0.0",
|
"apscheduler<4.0.0",
|
||||||
"discord-py",
|
"discord-py",
|
||||||
"audioop-lts",
|
"audioop-lts",
|
||||||
|
"discord-webhook",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@ -45,13 +46,13 @@ requires = ["poetry-core>=1.0.0"]
|
|||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
# https://docs.astral.sh/ruff/linter/
|
|
||||||
preview = true
|
preview = true
|
||||||
|
line-length = 120
|
||||||
# Enable all rules
|
|
||||||
lint.select = ["ALL"]
|
lint.select = ["ALL"]
|
||||||
|
lint.pydocstyle.convention = "google"
|
||||||
|
lint.isort.required-imports = ["from __future__ import annotations"]
|
||||||
|
lint.pycodestyle.ignore-overlong-task-comments = true
|
||||||
|
|
||||||
# Ignore some rules
|
|
||||||
lint.ignore = [
|
lint.ignore = [
|
||||||
"CPY001", # Checks for the absence of copyright notices within Python files.
|
"CPY001", # Checks for the absence of copyright notices within Python files.
|
||||||
"D100", # Checks for undocumented public module definitions.
|
"D100", # Checks for undocumented public module definitions.
|
||||||
@ -78,24 +79,13 @@ lint.ignore = [
|
|||||||
"W191", # Checks for indentation that uses tabs.
|
"W191", # Checks for indentation that uses tabs.
|
||||||
]
|
]
|
||||||
|
|
||||||
# https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
|
|
||||||
lint.pydocstyle.convention = "google"
|
|
||||||
|
|
||||||
# Add "from __future__ import annotations" to all files
|
|
||||||
lint.isort.required-imports = ["from __future__ import annotations"]
|
|
||||||
|
|
||||||
lint.pycodestyle.ignore-overlong-task-comments = true
|
|
||||||
|
|
||||||
# Default is 88 characters
|
|
||||||
line-length = 120
|
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
# https://docs.astral.sh/ruff/formatter/
|
|
||||||
docstring-code-format = true
|
docstring-code-format = true
|
||||||
docstring-code-line-length = 20
|
docstring-code-line-length = 20
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"**/*_test.py" = [
|
"**/test_*.py" = [
|
||||||
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
|
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
|
||||||
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
|
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
|
||||||
"PLR2004", # Magic value used in comparison, ...
|
"PLR2004", # Magic value used in comparison, ...
|
||||||
@ -103,13 +93,10 @@ docstring-code-line-length = 20
|
|||||||
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||||
]
|
]
|
||||||
|
|
||||||
# https://pytest-django.readthedocs.io/en/latest/
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
# Enable logging in the console.
|
|
||||||
log_cli = true
|
log_cli = true
|
||||||
log_cli_level = "INFO"
|
log_cli_level = "INFO"
|
||||||
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
||||||
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
# Only test files with the following suffixes.
|
|
||||||
python_files = "test_*.py *_test.py *_tests.py"
|
python_files = "test_*.py *_test.py *_tests.py"
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import dateparser
|
|
||||||
import pytz
|
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
||||||
|
|
||||||
from discord_reminder_bot.main import send_to_discord
|
|
||||||
|
|
||||||
|
|
||||||
class TestCountdown:
|
|
||||||
"""This tests everything.
|
|
||||||
|
|
||||||
This sets up sqlite database in memory, changes scheduler timezone
|
|
||||||
to Europe/Stockholm and creates job that runs January 18 2040 and one that
|
|
||||||
runs at 00:00.
|
|
||||||
"""
|
|
||||||
|
|
||||||
jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
|
|
||||||
job_defaults: dict[str, bool] = {"coalesce": True}
|
|
||||||
scheduler = AsyncIOScheduler(
|
|
||||||
jobstores=jobstores,
|
|
||||||
timezone=pytz.timezone("Europe/Stockholm"),
|
|
||||||
job_defaults=job_defaults,
|
|
||||||
)
|
|
||||||
|
|
||||||
parsed_date: datetime | None = dateparser.parse(
|
|
||||||
"18 January 2040",
|
|
||||||
settings={
|
|
||||||
"PREFER_DATES_FROM": "future",
|
|
||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert parsed_date
|
|
||||||
|
|
||||||
run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
job: Job = scheduler.add_job(
|
|
||||||
send_to_discord,
|
|
||||||
run_date=run_date,
|
|
||||||
kwargs={
|
|
||||||
"channel_id": 865712621109772329,
|
|
||||||
"message": "Running PyTest",
|
|
||||||
"author_id": 126462229892694018,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
timezone_date: datetime | None = dateparser.parse(
|
|
||||||
"00:00",
|
|
||||||
settings={
|
|
||||||
"PREFER_DATES_FROM": "future",
|
|
||||||
"TIMEZONE": "Europe/Stockholm",
|
|
||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert timezone_date
|
|
||||||
timezone_run_date: str = timezone_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
timezone_job: Job = scheduler.add_job(
|
|
||||||
send_to_discord,
|
|
||||||
run_date=timezone_run_date,
|
|
||||||
kwargs={
|
|
||||||
"channel_id": 865712621109772329,
|
|
||||||
"message": "Running PyTest at 00:00",
|
|
||||||
"author_id": 126462229892694018,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
timezone_date2: datetime | None = dateparser.parse(
|
|
||||||
"13:37",
|
|
||||||
settings={
|
|
||||||
"PREFER_DATES_FROM": "future",
|
|
||||||
"TIMEZONE": "Europe/Stockholm",
|
|
||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert timezone_date2
|
|
||||||
timezone_run_date2: str = timezone_date2.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
timezone_job2: Job = scheduler.add_job(
|
|
||||||
send_to_discord,
|
|
||||||
run_date=timezone_run_date2,
|
|
||||||
kwargs={
|
|
||||||
"channel_id": 865712621109772329,
|
|
||||||
"message": "Running PyTest at 13:37",
|
|
||||||
"author_id": 126462229892694018,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_if_timezones_are_working(self) -> None: # noqa: ANN101
|
|
||||||
"""Check if timezones are working.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
self: TestCountdown
|
|
||||||
"""
|
|
||||||
time_job: Job | None = self.scheduler.get_job(self.timezone_job.id)
|
|
||||||
assert time_job
|
|
||||||
|
|
||||||
assert time_job.trigger.run_date.hour == 0
|
|
||||||
assert time_job.trigger.run_date.minute == 0
|
|
||||||
assert time_job.trigger.run_date.second == 0
|
|
||||||
|
|
||||||
time_job2: Job | None = self.scheduler.get_job(self.timezone_job2.id)
|
|
||||||
assert time_job2
|
|
||||||
|
|
||||||
assert time_job2.trigger.run_date.hour == 13 # noqa: PLR2004
|
|
||||||
assert time_job2.trigger.run_date.minute == 37 # noqa: PLR2004
|
|
||||||
assert time_job2.trigger.run_date.second == 0
|
|
@ -1,192 +0,0 @@
|
|||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import dateparser
|
|
||||||
import interactions
|
|
||||||
import pytz
|
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
||||||
from interactions.ext.paginator import Page
|
|
||||||
|
|
||||||
from discord_reminder_bot.create_pages import (
|
|
||||||
_get_pages,
|
|
||||||
_get_pause_or_unpause_button,
|
|
||||||
_get_row_of_buttons,
|
|
||||||
_get_trigger_text,
|
|
||||||
_make_button,
|
|
||||||
_pause_job,
|
|
||||||
_unpause_job,
|
|
||||||
)
|
|
||||||
from discord_reminder_bot.main import send_to_discord
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from collections.abc import Generator
|
|
||||||
|
|
||||||
|
|
||||||
def _test_pause_unpause_button(job: Job, button_label: str) -> None:
|
|
||||||
button2: interactions.Button | None = _get_pause_or_unpause_button(job)
|
|
||||||
assert button2
|
|
||||||
assert button2.label == button_label
|
|
||||||
assert button2.style == interactions.ButtonStyle.PRIMARY
|
|
||||||
assert button2.type == interactions.ComponentType.BUTTON
|
|
||||||
assert button2.emoji is None
|
|
||||||
assert button2.custom_id == button_label.lower()
|
|
||||||
assert button2.url is None
|
|
||||||
assert button2.disabled is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestCountdown:
|
|
||||||
jobstores: dict[str, SQLAlchemyJobStore] = {"default": SQLAlchemyJobStore(url="sqlite:///:memory")}
|
|
||||||
job_defaults: dict[str, bool] = {"coalesce": True}
|
|
||||||
scheduler = AsyncIOScheduler(
|
|
||||||
jobstores=jobstores,
|
|
||||||
timezone=pytz.timezone("Europe/Stockholm"),
|
|
||||||
job_defaults=job_defaults,
|
|
||||||
)
|
|
||||||
|
|
||||||
parsed_date: datetime | None = dateparser.parse(
|
|
||||||
"18 January 2040",
|
|
||||||
settings={
|
|
||||||
"PREFER_DATES_FROM": "future",
|
|
||||||
"TO_TIMEZONE": "Europe/Stockholm",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert parsed_date
|
|
||||||
|
|
||||||
run_date: str = parsed_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
normal_job: Job = scheduler.add_job(
|
|
||||||
send_to_discord,
|
|
||||||
run_date=run_date,
|
|
||||||
kwargs={
|
|
||||||
"channel_id": 865712621109772329,
|
|
||||||
"message": "Running PyTest",
|
|
||||||
"author_id": 126462229892694018,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
cron_job: Job = scheduler.add_job(
|
|
||||||
send_to_discord,
|
|
||||||
"cron",
|
|
||||||
minute="0",
|
|
||||||
kwargs={
|
|
||||||
"channel_id": 865712621109772329,
|
|
||||||
"message": "Running PyTest",
|
|
||||||
"author_id": 126462229892694018,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
interval_job: Job = scheduler.add_job(
|
|
||||||
send_to_discord,
|
|
||||||
"interval",
|
|
||||||
minutes=1,
|
|
||||||
kwargs={
|
|
||||||
"channel_id": 865712621109772329,
|
|
||||||
"message": "Running PyTest",
|
|
||||||
"author_id": 126462229892694018,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_trigger_text(self) -> None: # noqa: ANN101
|
|
||||||
# FIXME: This try except train should be replaced with a better solution lol
|
|
||||||
trigger_text: str = _get_trigger_text(self.normal_job)
|
|
||||||
try:
|
|
||||||
regex: str = r"2040-01-18 \d+:00 \(in \d+ days, \d+ hours, \d+ minutes\)"
|
|
||||||
assert re.match(regex, trigger_text)
|
|
||||||
except AssertionError:
|
|
||||||
try:
|
|
||||||
regex2: str = r"2040-01-18 \d+:00 \(in \d+ days, \d+ minutes\)"
|
|
||||||
assert re.match(regex2, trigger_text)
|
|
||||||
except AssertionError:
|
|
||||||
regex3: str = r"2040-01-18 \d+:00 \(in \d+ days, \d+ hours\)"
|
|
||||||
assert re.match(regex3, trigger_text)
|
|
||||||
|
|
||||||
def test_make_button(self) -> None: # noqa: ANN101
|
|
||||||
button_name: str = "Test"
|
|
||||||
|
|
||||||
button: interactions.Button = _make_button(label=button_name, style=interactions.ButtonStyle.PRIMARY)
|
|
||||||
assert button.label == button_name
|
|
||||||
assert button.style == interactions.ButtonStyle.PRIMARY
|
|
||||||
assert button.custom_id == button_name.lower()
|
|
||||||
assert button.disabled is None
|
|
||||||
assert button.emoji is None
|
|
||||||
|
|
||||||
def test_get_pause_or_unpause_button(self) -> None: # noqa: ANN101
|
|
||||||
button: interactions.Button | None = _get_pause_or_unpause_button(self.normal_job)
|
|
||||||
assert button is None
|
|
||||||
|
|
||||||
_test_pause_unpause_button(self.cron_job, "Pause")
|
|
||||||
self.cron_job.pause()
|
|
||||||
|
|
||||||
_test_pause_unpause_button(self.cron_job, "Unpause")
|
|
||||||
self.cron_job.resume()
|
|
||||||
|
|
||||||
_test_pause_unpause_button(self.interval_job, "Pause")
|
|
||||||
self.interval_job.pause()
|
|
||||||
|
|
||||||
_test_pause_unpause_button(self.interval_job, "Unpause")
|
|
||||||
self.interval_job.resume()
|
|
||||||
|
|
||||||
def test_get_row_of_buttons(self) -> None: # noqa: ANN101
|
|
||||||
row: interactions.ActionRow = _get_row_of_buttons(self.normal_job)
|
|
||||||
assert row
|
|
||||||
assert row.components
|
|
||||||
|
|
||||||
# A normal job should have 2 buttons, edit and delete
|
|
||||||
assert len(row.components) == 2 # noqa: PLR2004
|
|
||||||
|
|
||||||
row2: interactions.ActionRow = _get_row_of_buttons(self.cron_job)
|
|
||||||
assert row2
|
|
||||||
assert row2.components
|
|
||||||
|
|
||||||
# A cron job should have 3 buttons, edit, delete and pause/unpause
|
|
||||||
assert len(row2.components) == 3 # noqa: PLR2004
|
|
||||||
|
|
||||||
# A cron job should have 3 buttons, edit, delete and pause/unpause
|
|
||||||
assert len(row2.components) == 3 # noqa: PLR2004
|
|
||||||
|
|
||||||
def test_get_pages(self) -> None: # noqa: ANN101
|
|
||||||
ctx = None # TODO: We should check ctx as well and not only channel id
|
|
||||||
channel: interactions.Channel = interactions.Channel(id=interactions.Snowflake(865712621109772329))
|
|
||||||
|
|
||||||
pages: Generator[Page, None, None] = _get_pages(job=self.normal_job, channel=channel, ctx=ctx) # type: ignore # noqa: PGH003, E501
|
|
||||||
assert pages
|
|
||||||
|
|
||||||
for page in pages:
|
|
||||||
assert page
|
|
||||||
assert page.title == "Running PyTest"
|
|
||||||
assert page.components
|
|
||||||
assert page.embeds
|
|
||||||
assert page.embeds.fields is not None # type: ignore # noqa: PGH003
|
|
||||||
assert page.embeds.fields[0].name == "**Channel:**" # type: ignore # noqa: PGH003
|
|
||||||
assert page.embeds.fields[0].value == "#" # type: ignore # noqa: PGH003
|
|
||||||
assert page.embeds.fields[1].name == "**Message:**" # type: ignore # noqa: PGH003
|
|
||||||
assert page.embeds.fields[1].value == "Running PyTest" # type: ignore # noqa: PGH003
|
|
||||||
assert page.embeds.fields[2].name == "**Trigger:**" # type: ignore # noqa: PGH003
|
|
||||||
trigger_text: str = page.embeds.fields[2].value # type: ignore # noqa: PGH003
|
|
||||||
|
|
||||||
# FIXME: This try except train should be replaced with a better solution lol
|
|
||||||
try:
|
|
||||||
regex: str = r"2040-01-18 \d+:00 \(in \d+ days, \d+ hours, \d+ minutes\)"
|
|
||||||
assert re.match(regex, trigger_text)
|
|
||||||
except AssertionError:
|
|
||||||
try:
|
|
||||||
regex2: str = r"2040-01-18 \d+:00 \(in \d+ days, \d+ minutes\)"
|
|
||||||
assert re.match(regex2, trigger_text)
|
|
||||||
except AssertionError:
|
|
||||||
regex3: str = r"2040-01-18 \d+:00 \(in \d+ days, \d+ hours\)"
|
|
||||||
assert re.match(regex3, trigger_text)
|
|
||||||
|
|
||||||
# Check if type is Page
|
|
||||||
assert isinstance(page, Page)
|
|
||||||
|
|
||||||
def test_pause_job(self) -> None: # noqa: ANN101
|
|
||||||
assert _pause_job(self.interval_job, self.scheduler) == f"Job {self.interval_job.id} paused."
|
|
||||||
assert _pause_job(self.cron_job, self.scheduler) == f"Job {self.cron_job.id} paused."
|
|
||||||
assert _pause_job(self.normal_job, self.scheduler) == f"Job {self.normal_job.id} paused."
|
|
||||||
|
|
||||||
def test_unpause_job(self) -> None: # noqa: ANN101
|
|
||||||
assert _unpause_job(self.interval_job, self.scheduler) == f"Job {self.interval_job.id} unpaused."
|
|
||||||
assert _unpause_job(self.cron_job, self.scheduler) == f"Job {self.cron_job.id} unpaused."
|
|
||||||
assert _unpause_job(self.normal_job, self.scheduler) == f"Job {self.normal_job.id} unpaused."
|
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from discord_reminder_bot import main
|
from discord_reminder_bot import main
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import tzlocal
|
|
||||||
|
|
||||||
from discord_reminder_bot.parse import ParsedTime, parse_time
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_time() -> None:
|
|
||||||
"""Test the parse_time function."""
|
|
||||||
parsed_time: ParsedTime = parse_time("18 January 2040")
|
|
||||||
assert parsed_time.err is False
|
|
||||||
assert not parsed_time.err_msg
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040"
|
|
||||||
assert parsed_time.parsed_time
|
|
||||||
assert parsed_time.parsed_time.strftime("%Y-%m-%d %H:%M:%S") == "2040-01-18 00:00:00"
|
|
||||||
|
|
||||||
parsed_time: ParsedTime = parse_time("18 January 2040 12:00")
|
|
||||||
assert parsed_time.err is False
|
|
||||||
assert not parsed_time.err_msg
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040 12:00"
|
|
||||||
assert parsed_time.parsed_time
|
|
||||||
assert parsed_time.parsed_time.strftime("%Y-%m-%d %H:%M:%S") == "2040-01-18 12:00:00"
|
|
||||||
|
|
||||||
parsed_time: ParsedTime = parse_time("18 January 2040 12:00:00")
|
|
||||||
assert parsed_time.err is False
|
|
||||||
assert not parsed_time.err_msg
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040 12:00:00"
|
|
||||||
assert parsed_time.parsed_time
|
|
||||||
assert parsed_time.parsed_time.strftime("%Y-%m-%d %H:%M:%S") == "2040-01-18 12:00:00"
|
|
||||||
|
|
||||||
parsed_time: ParsedTime = parse_time("18 January 2040 12:00:00 UTC")
|
|
||||||
assert parsed_time.err is False
|
|
||||||
assert not parsed_time.err_msg
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040 12:00:00 UTC"
|
|
||||||
assert parsed_time.parsed_time
|
|
||||||
assert parsed_time.parsed_time.strftime("%Y-%m-%d %H:%M:%S") == "2040-01-18 13:00:00"
|
|
||||||
|
|
||||||
parsed_time: ParsedTime = parse_time("18 January 2040 12:00:00 Europe/Stockholm")
|
|
||||||
assert parsed_time.err is True
|
|
||||||
assert parsed_time.err_msg == "Could not parse the date."
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040 12:00:00 Europe/Stockholm"
|
|
||||||
assert parsed_time.parsed_time is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_ParsedTime() -> None: # noqa: N802
|
|
||||||
"""Test the ParsedTime class."""
|
|
||||||
parsed_time: ParsedTime = ParsedTime(
|
|
||||||
err=False,
|
|
||||||
err_msg="",
|
|
||||||
date_to_parse="18 January 2040",
|
|
||||||
parsed_time=datetime(2040, 1, 18, 0, 0, 0, tzinfo=tzlocal.get_localzone()),
|
|
||||||
)
|
|
||||||
assert parsed_time.err is False
|
|
||||||
assert not parsed_time.err_msg
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040"
|
|
||||||
assert parsed_time.parsed_time
|
|
||||||
assert parsed_time.parsed_time.strftime("%Y-%m-%d %H:%M:%S") == "2040-01-18 00:00:00"
|
|
||||||
|
|
||||||
parsed_time: ParsedTime = ParsedTime(
|
|
||||||
err=True,
|
|
||||||
err_msg="Could not parse the date.",
|
|
||||||
date_to_parse="18 January 2040 12:00:00 Europe/Stockholm",
|
|
||||||
parsed_time=None,
|
|
||||||
)
|
|
||||||
assert parsed_time.err is True
|
|
||||||
assert parsed_time.err_msg == "Could not parse the date."
|
|
||||||
assert parsed_time.date_to_parse == "18 January 2040 12:00:00 Europe/Stockholm"
|
|
||||||
assert parsed_time.parsed_time is None
|
|
Reference in New Issue
Block a user