diff --git a/kolkra_ng/bot.py b/kolkra_ng/bot.py index 1652cad..0e3e980 100644 --- a/kolkra_ng/bot.py +++ b/kolkra_ng/bot.py @@ -29,7 +29,8 @@ class Kolkra(commands.Bot): def __init__(self, config: Config) -> None: super().__init__( command_prefix=commands.when_mentioned_or(";"), - intents=Intents.default() | Intents(members=True, message_content=True), + intents=Intents.default() + | Intents(members=True, message_content=True, presences=True), ) self.config = config self.motor = AsyncIOMotorClient( @@ -83,6 +84,8 @@ async def load_modules(self) -> None: ) try: await self.load_extension(module) + except NotImplementedError: + log.info("Skipped unfinished module %s", module) except Exception as e: log.warning("Failed to load module %s", module, exc_info=e) else: diff --git a/kolkra_ng/cogs/mod/mod_actions/warning.py b/kolkra_ng/cogs/mod/mod_actions/warning.py index 47c8a29..8d8178d 100644 --- a/kolkra_ng/cogs/mod/mod_actions/warning.py +++ b/kolkra_ng/cogs/mod/mod_actions/warning.py @@ -66,7 +66,9 @@ async def dm_embed(self, bot: Kolkra) -> Embed: return embed def log_base(self) -> Embed: - return Embed(title="Warning", color=Color.yellow()).set_thumbnail(url=icons8("error")) + return Embed(title="Warning", color=Color.yellow()).set_thumbnail( + url=icons8("error") + ) async def log_embed(self) -> Embed: count = await self.cached_count() diff --git a/kolkra_ng/cogs/translate/__init__.py b/kolkra_ng/cogs/translate/__init__.py new file mode 100644 index 0000000..9cfcda0 --- /dev/null +++ b/kolkra_ng/cogs/translate/__init__.py @@ -0,0 +1,128 @@ +"""Because every other bot we've tried can go f**k themselves. +""" + +import logging +from uuid import UUID + +from aiohttp import ClientSession +from discord import Color, Embed, Message +from discord.ext import commands +from pydantic import BaseModel, Field, HttpUrl, Secret +from pydantic_extra_types.language_code import LanguageAlpha2 + +from kolkra_ng.bot import Kolkra +from kolkra_ng.cogs.translate.api import ( + DetectRequest, + DetectResponseItem, + LanguagesResponseItem, + TranslateRequest, + TranslateResponse, +) +from kolkra_ng.embeds import WarningEmbed, icons8 + +log = logging.getLogger(__name__) + + +class TranslateConfig(BaseModel): + target_language: LanguageAlpha2 = Field( + default="en", + description="The language to translate foreign-language messages to. Defaults to 'en' (English).", + ) + api_base_url: HttpUrl = Field( + description="Base URL of a [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) instance.", + ) + api_key: Secret[UUID] | None = Field( + default=None, + description="An API key for the LibreTranslate instance, if required.", + ) + aiohttp_params: dict = Field( + default_factory=dict, + description="Extra parameters to pass to the aiohttp.ClientSession constructor.", + ) + + +class TranslateCog(commands.Cog): + def __init__(self, bot: Kolkra) -> None: + super().__init__() + self.bot = bot + self.config = TranslateConfig(**bot.config.cogs.get(self.__cog_name__, {})) + self.session = ClientSession( + base_url=self.config.api_base_url.unicode_string(), + **self.config.aiohttp_params, + ) + + async def cog_load(self) -> None: + async with self.session.get("/languages") as resp: + resp.raise_for_status() + languages_response = [ + LanguagesResponseItem(**item) for item in await resp.json() + ] + self.all_languages = { + language.code: language for language in languages_response + } + self.supported_languages = { + code: language + for code, language in self.all_languages.items() + if self.config.target_language in language.targets + } + + async def cog_unload(self) -> None: + await self.session.close() + + @commands.Cog.listener() + async def on_message(self, message: Message) -> None: + if message.author.bot or not message.content: + return + async with self.session.post( + "/detect", + data=DetectRequest( + q=message.content, + api_key=( + key.get_secret_value() if (key := self.config.api_key) else None + ), + ).model_dump(mode="json", exclude_none=True), + ) as resp: + resp.raise_for_status() + detect_response = [DetectResponseItem(**item) for item in await resp.json()] + detected_language = max(detect_response, key=lambda x: x.confidence) + if detected_language.language == self.config.target_language: + return + elif detected_language.language not in self.supported_languages: + await message.reply( + embed=WarningEmbed( + title="Language not supported!", + description=f"This message's detected language ({self.all_languages[detected_language.language].name}) cannot be translated to {self.all_languages[detected_language.language].name}.", + ), + mention_author=False, + ) + return + async with self.session.post( + "/translate", + data=TranslateRequest( + q=message.content, + api_key=( + key.get_secret_value() if (key := self.config.api_key) else None + ), + source=detected_language.language, + target=self.config.target_language, + ).model_dump(mode="json", exclude_none=True), + ) as resp: + resp.raise_for_status() + translate_response = TranslateResponse(**await resp.json()) + await message.reply( + embed=Embed( + color=Color.blue(), + title="Automatic translation", + description=translate_response.translatedText, + ) + .add_field( + name="Language", + value=f"{self.all_languages[detected_language.language].name} -> {self.all_languages[self.config.target_language].name}", + ) + .set_thumbnail(url=icons8("translate-text")) + .set_footer(text="Machine translations may not be 100% accurate.") + ) + + +async def setup(bot: Kolkra) -> None: + await bot.add_cog(TranslateCog(bot)) diff --git a/kolkra_ng/cogs/translate/api.py b/kolkra_ng/cogs/translate/api.py new file mode 100644 index 0000000..36759a6 --- /dev/null +++ b/kolkra_ng/cogs/translate/api.py @@ -0,0 +1,36 @@ +from typing import Literal +from uuid import UUID + +from pydantic import BaseModel, Field, NonNegativeInt +from pydantic_extra_types.language_code import LanguageAlpha2 + + +class DetectRequest(BaseModel): + q: str + api_key: UUID | None + + +class DetectResponseItem(BaseModel): + confidence: int = Field(ge=0, le=100) + language: LanguageAlpha2 + + +class LanguagesResponseItem(BaseModel): + code: LanguageAlpha2 + name: str + targets: set[LanguageAlpha2] + + +class TranslateRequest(BaseModel): + q: str + source: LanguageAlpha2 | Literal["auto"] + target: LanguageAlpha2 + format: Literal["text", "html"] = "text" + alternatives: NonNegativeInt = 0 + api_key: UUID | None + + +class TranslateResponse(BaseModel): + translatedText: str + detectedLanguage: DetectResponseItem | None = None + alternatives: list[str] | None = None diff --git a/kolkra_ng/enums/by_name.py b/kolkra_ng/enums/by_name.py index db81c8b..9ae4e3d 100644 --- a/kolkra_ng/enums/by_name.py +++ b/kolkra_ng/enums/by_name.py @@ -36,6 +36,7 @@ def __get_pydantic_json_schema__( def by_name(cls: E) -> E: + """Decorator to make Pydantic handle an enum by name rather than by value.""" cls.__get_pydantic_core_schema__ = ( # pyright: ignore # noqa: PGH003 types.MethodType(__get_pydantic_core_schema__, cls) diff --git a/poetry.lock b/poetry.lock index e6b6f9d..250f98e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -190,6 +190,52 @@ doc = ["Markdown (>=3.3)", "Pygments (>=2.8.0)", "jinja2 (>=3.0.3)", "mkdocs (>= queue = ["beanie-batteries-queue (>=0.2)"] test = ["asgi-lifespan (>=1.0.1)", "dnspython (>=2.1.0)", "fastapi (>=0.100)", "flake8 (>=3)", "httpx (>=0.23.0)", "pre-commit (>=2.3.0)", "pydantic-extra-types (>=2)", "pydantic-settings (>=2)", "pydantic[email]", "pyright (>=0)", "pytest (>=6.0.0)", "pytest-asyncio (>=0.21.0)", "pytest-cov (>=2.8.1)"] +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "braceexpand" version = "0.1.7" @@ -359,7 +405,7 @@ voice = ["PyNaCl (>=1.3.0,<1.6)"] type = "git" url = "https://github.com/Rapptz/discord.py.git" reference = "master" -resolved_reference = "0e58a927ddbc300a17ef0137d948faa659565313" +resolved_reference = "c055fd32bbe5c68b144a7ac938b911035eb6d3e1" [[package]] name = "distlib" @@ -394,13 +440,13 @@ wmi = ["wmi (>=1.5.1)"] [[package]] name = "email-validator" -version = "2.1.1" +version = "2.1.2" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, - {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, + {file = "email_validator-2.1.2-py3-none-any.whl", hash = "sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115"}, + {file = "email_validator-2.1.2.tar.gz", hash = "sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c"}, ] [package.dependencies] @@ -440,18 +486,18 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.14.0" +version = "3.15.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, + {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, + {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -1074,6 +1120,17 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1152,15 +1209,26 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "phonenumbers" -version = "8.13.38" +version = "8.13.39" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = "*" files = [ - {file = "phonenumbers-8.13.38-py2.py3-none-any.whl", hash = "sha256:d22aa747fb591ef2a18afec13cab5a0e294ab20fce5a1560e4949e459e70eeef"}, - {file = "phonenumbers-8.13.38.tar.gz", hash = "sha256:2822c74ee9334e9d8ad792fc352cc8d21004307349b6b1bb61da12937fa2eaba"}, + {file = "phonenumbers-8.13.39-py2.py3-none-any.whl", hash = "sha256:3ad2d086fa71e7eef409001b9195ac54bebb0c6e3e752209b558ca192c9229a0"}, + {file = "phonenumbers-8.13.39.tar.gz", hash = "sha256:db7ca4970d206b2056231105300753b1a5b229f43416f8c2b3010e63fbb68d77"}, ] [[package]] @@ -1352,15 +1420,26 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "pycountry" +version = "24.6.1" +description = "ISO country, subdivision, language, currency and script definitions and their translations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycountry-24.6.1-py3-none-any.whl", hash = "sha256:f1a4fb391cd7214f8eefd39556d740adcc233c778a27f8942c8dca351d6ce06f"}, + {file = "pycountry-24.6.1.tar.gz", hash = "sha256:b61b3faccea67f87d10c1f2b0fc0be714409e8fcdcc1315613174f6466c10221"}, +] + [[package]] name = "pydantic" -version = "2.7.3" +version = "2.7.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, - {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, + {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, + {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, ] [package.dependencies] @@ -1465,13 +1544,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-extra-types" -version = "2.8.0" +version = "2.8.2" description = "Extra Pydantic types." optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_extra_types-2.8.0-py3-none-any.whl", hash = "sha256:39cc533f996514966d885e7a54af4b52e19160a5a7c36fd585a44a5ed16ec7be"}, - {file = "pydantic_extra_types-2.8.0.tar.gz", hash = "sha256:17a677c4e45716a7d8f347e91c411af4954b4e92042f24973b96a140581ba706"}, + {file = "pydantic_extra_types-2.8.2-py3-none-any.whl", hash = "sha256:f2400b3c3553fb7fa09a131967b4edf2d53f01ad9fa89d158784653f2e5c13d1"}, + {file = "pydantic_extra_types-2.8.2.tar.gz", hash = "sha256:4d2b3c52c1e2e4dfa31bf1d5a37b841b09e3c5a08ec2bffca0e07fc2ad7d5c4a"}, ] [package.dependencies] @@ -1595,13 +1674,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyright" -version = "1.1.366" +version = "1.1.367" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.366-py3-none-any.whl", hash = "sha256:c09e73ccc894976bcd6d6a5784aa84d724dbd9ceb7b873b39d475ca61c2de071"}, - {file = "pyright-1.1.366.tar.gz", hash = "sha256:10e4d60be411f6d960cd39b0b58bf2ff76f2c83b9aeb102ffa9d9fda2e1303cb"}, + {file = "pyright-1.1.367-py3-none-any.whl", hash = "sha256:89de6502ae02f1552d0c4df4b46867887a419849f379db617695ef9308cf01eb"}, + {file = "pyright-1.1.367.tar.gz", hash = "sha256:b1e5522ceb246ee6bc293a43d6d0162719d6467c1f1e9b81cee741aa11cdacbd"}, ] [package.dependencies] @@ -1925,6 +2004,34 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +[[package]] +name = "uritools" +version = "4.0.3" +description = "URI parsing, classification and composition" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uritools-4.0.3-py3-none-any.whl", hash = "sha256:bae297d090e69a0451130ffba6f2f1c9477244aa0a5543d66aed2d9f77d0dd9c"}, + {file = "uritools-4.0.3.tar.gz", hash = "sha256:ee06a182a9c849464ce9d5fa917539aacc8edd2a4924d1b7aabeeecabcae3bc2"}, +] + +[[package]] +name = "urlextract" +version = "1.9.0" +description = "Collects and extracts URLs from given text." +optional = false +python-versions = "*" +files = [ + {file = "urlextract-1.9.0-py3-none-any.whl", hash = "sha256:f88963532488b1c7c405e21bd162ae97871754ea04b60e18d33ee075b19b82fd"}, + {file = "urlextract-1.9.0.tar.gz", hash = "sha256:70508e02ba9df372e25cf0642db367cece273e8712cd0ce78178fc5dd7ea00db"}, +] + +[package.dependencies] +filelock = "*" +idna = "*" +platformdirs = "*" +uritools = "*" + [[package]] name = "virtualenv" version = "20.26.2" @@ -2094,4 +2201,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "3a425adb97e19becc4ad68d31e3d848d361dfc317cc3a7e22847697559703ac3" +content-hash = "f4a6c92e115310b5268348d70a09c440ec02f23bb912133f3b90c33192ee9669" diff --git a/pyproject.toml b/pyproject.toml index f04961f..a19d160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,12 +28,15 @@ pytimeparse = "^1.1.8" python-frontmatter = "^1.1.0" pint = ">=0.7" # Versions prior to this use eval for parsing string input--BAD NEWS tomli = "^2.0.1" +urlextract = "^1.9.0" +pycountry = "^24.6.1" [tool.poetry.group.dev.dependencies] pytest = "^7.2.0" pre-commit = "^3.4.0" pyright = "^1.1.355" matplotlib = "^3.8.4" +black = "^24.4.2" [tool.pyright] typeCheckingMode = "standard"