Compare commits

...

26 Commits

Author SHA1 Message Date
88786131f1 Merge branch 'dev' into profitroll/sphinx
# Conflicts:
#	src/libbot/i18n/_functions.py
#	src/libbot/i18n/classes/bot_locale.py
2025-07-09 14:39:19 +02:00
ef7380ae45 WIP: Documentation improvement and format change to Google
Some checks failed
Analysis / SonarCloud (push) Successful in 54s
Analysis / SonarCloud (pull_request) Successful in 48s
Tests / Build and Test (3.11) (pull_request) Failing after 1m2s
Tests / Build and Test (3.12) (pull_request) Failing after 1m0s
Tests / Build and Test (3.13) (pull_request) Failing after 59s
2025-07-09 14:32:50 +02:00
727d531d63 Added support for default_ttl_seconds in create_cache_client() 2025-07-09 14:31:55 +02:00
7d95b1efee Merge pull request 'Update dependency types-aiofiles to v24.1.0.20250708' (#222) from renovate/types-aiofiles-24.x into dev
All checks were successful
Analysis / SonarCloud (push) Successful in 57s
Reviewed-on: #222
2025-07-08 15:45:53 +03:00
03115b4059 Update dependency types-aiofiles to v24.1.0.20250708
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 44s
Tests / Build and Test (3.11) (pull_request) Successful in 1m12s
Tests / Build and Test (3.12) (pull_request) Successful in 1m13s
Tests / Build and Test (3.13) (pull_request) Successful in 1m7s
2025-07-08 06:48:18 +03:00
6038b5058f Merge pull request 'Update project for sphinx to use libbot 4.3.0' (#221) from dev into profitroll/sphinx
Reviewed-on: #221
2025-07-08 01:42:28 +03:00
e4ce5976f2 Changed default cache TTL to 0 and None for memcached and redis respectively
All checks were successful
Analysis / SonarCloud (push) Successful in 50s
Analysis / SonarCloud (pull_request) Successful in 52s
Tests / Build and Test (3.11) (pull_request) Successful in 1m14s
Tests / Build and Test (3.12) (pull_request) Successful in 1m20s
Tests / Build and Test (3.13) (pull_request) Successful in 1m18s
2025-07-08 00:01:14 +02:00
32a9e14d0c Added a default TTL of 300 seconds for cache entries (#219)
All checks were successful
Analysis / SonarCloud (push) Successful in 50s
Analysis / SonarCloud (pull_request) Successful in 1m4s
Tests / Build and Test (3.11) (pull_request) Successful in 1m14s
Tests / Build and Test (3.12) (pull_request) Successful in 1m15s
Tests / Build and Test (3.13) (pull_request) Successful in 1m19s
2025-07-07 23:47:55 +02:00
3110bb64b1 Closes #219
All checks were successful
Analysis / SonarCloud (push) Successful in 1m35s
2025-07-07 23:35:21 +02:00
ad38dbdca1 Update dependency pytest to v8.4.1
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 50s
Tests / Build and Test (3.11) (pull_request) Successful in 1m17s
Tests / Build and Test (3.12) (pull_request) Successful in 1m15s
Tests / Build and Test (3.13) (pull_request) Successful in 1m9s
Analysis / SonarCloud (push) Successful in 50s
2025-06-18 09:21:02 +03:00
edc3e0717d Update dependency tox to v4.27.0
All checks were successful
Tests / Build and Test (3.11) (pull_request) Successful in 1m22s
Tests / Build and Test (3.12) (pull_request) Successful in 1m30s
Tests / Build and Test (3.13) (pull_request) Successful in 1m40s
Analysis / SonarCloud (push) Successful in 1m1s
Analysis / SonarCloud (pull_request) Successful in 58s
2025-06-17 18:51:05 +03:00
d70fd4f491 Update dependency mypy to v1.16.1
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 58s
Tests / Build and Test (3.11) (pull_request) Successful in 1m17s
Tests / Build and Test (3.12) (pull_request) Successful in 1m16s
Tests / Build and Test (3.13) (pull_request) Successful in 1m10s
Analysis / SonarCloud (push) Successful in 1m9s
2025-06-16 20:06:16 +03:00
c4fb1dd5dd Update dependency pytest-cov to v6.2.1
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 53s
Tests / Build and Test (3.11) (pull_request) Successful in 1m20s
Tests / Build and Test (3.12) (pull_request) Successful in 1m19s
Tests / Build and Test (3.13) (pull_request) Successful in 1m12s
Analysis / SonarCloud (push) Successful in 1m1s
2025-06-12 13:57:08 +03:00
38bf43a5e7 Update dependency pytest-cov to v6.2.0
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 50s
Tests / Build and Test (3.11) (pull_request) Successful in 1m24s
Tests / Build and Test (3.12) (pull_request) Successful in 1m34s
Tests / Build and Test (3.13) (pull_request) Successful in 1m11s
Analysis / SonarCloud (push) Successful in 49s
2025-06-12 01:32:32 +03:00
5dff5fa71d Update dependency types-aiofiles to v24.1.0.20250606
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 49s
Tests / Build and Test (3.11) (pull_request) Successful in 1m14s
Tests / Build and Test (3.12) (pull_request) Successful in 1m10s
Tests / Build and Test (3.13) (pull_request) Successful in 1m8s
Analysis / SonarCloud (push) Successful in 50s
2025-06-06 06:11:29 +03:00
e596658c68 Update dependency pytest to v8.4.0
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 58s
Tests / Build and Test (3.11) (pull_request) Successful in 1m11s
Tests / Build and Test (3.12) (pull_request) Successful in 1m8s
Tests / Build and Test (3.13) (pull_request) Successful in 1m6s
Analysis / SonarCloud (push) Successful in 51s
2025-06-02 20:43:25 +03:00
1e1b04a8ff Update dependency typing-extensions to ~=4.14.0
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 58s
Tests / Build and Test (3.11) (pull_request) Successful in 1m19s
Tests / Build and Test (3.12) (pull_request) Successful in 1m11s
Tests / Build and Test (3.13) (pull_request) Successful in 1m9s
Analysis / SonarCloud (push) Successful in 1m7s
2025-06-02 18:39:15 +03:00
69b034c007 Update dependency mypy to v1.16.0
All checks were successful
Analysis / SonarCloud (pull_request) Successful in 46s
Tests / Build and Test (3.11) (pull_request) Successful in 1m17s
Tests / Build and Test (3.12) (pull_request) Successful in 1m10s
Tests / Build and Test (3.13) (pull_request) Successful in 1m7s
Analysis / SonarCloud (push) Successful in 51s
2025-05-29 16:49:31 +03:00
kku
54de950899 Fixed compatibility issue with PEP 639
All checks were successful
Analysis / SonarCloud (push) Successful in 51s
2025-05-28 10:05:30 +02:00
c17a206c44 Update dependency redis to ~=6.2.0
Some checks failed
Analysis / SonarCloud (pull_request) Successful in 48s
Tests / Build and Test (3.11) (pull_request) Failing after 55s
Tests / Build and Test (3.12) (pull_request) Successful in 1m7s
Tests / Build and Test (3.13) (pull_request) Successful in 1m8s
Analysis / SonarCloud (push) Successful in 50s
2025-05-28 08:54:30 +03:00
dc05eb0ccb Update dependency pytest-asyncio to v1
Some checks failed
Analysis / SonarCloud (pull_request) Successful in 50s
Tests / Build and Test (3.11) (pull_request) Failing after 52s
Tests / Build and Test (3.12) (pull_request) Successful in 1m20s
Tests / Build and Test (3.13) (pull_request) Successful in 1m20s
Analysis / SonarCloud (push) Successful in 50s
2025-05-26 08:33:47 +03:00
3a7f748d96 Updated the license specification to comply with PEP 639
All checks were successful
Analysis / SonarCloud (push) Successful in 47s
2025-05-18 17:44:18 +02:00
646229d919 Added an index for the Sphinx builder 2025-01-05 22:57:08 +01:00
48fffb82ce Added basic files for the Sphinx builder 2025-01-05 22:48:15 +01:00
29a90fc385 WIP: Convert docstrings to Google's format 2025-01-05 22:46:40 +01:00
4777b3dc41 Added requirements for Sphinx 2025-01-04 17:45:31 +01:00
24 changed files with 492 additions and 277 deletions

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

61
docs/conf.py Normal file
View File

@@ -0,0 +1,61 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
from datetime import datetime
from typing import List, Dict, Any
import libbot
sys.path.insert(0, os.path.abspath(".."))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project: str = "LibBotUniversal"
author: str = libbot.__author__
version: str = libbot.__version__
release: str = version
copyright: str = f"{datetime.now().year} {author}"
html_title: str = f"{project} v{version} Documentation"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions: List[str] = [
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.viewcode",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx_copybutton",
]
templates_path: List[str] = ["_templates"]
exclude_patterns: List[str] = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme: str = "furo"
html_static_path: List[str] = ["_static"]
html_theme_options: Dict[str, Any] = {
"source_repository": "https://git.end-play.xyz/profitroll/LibBotUniversal",
"source_branch": "main",
"source_directory": "docs/",
"light_css_variables": {
"font-stack": "'Outfit', sans-serif",
},
}
resource_links: Dict[str, Any] = {
"issues": "https://git.end-play.xyz/profitroll/LibBotUniversal/issues",
"examples": "https://git.end-play.xyz/profitroll/LibBotUniversal/tree/main/examples",
}

29
docs/index.rst Normal file
View File

@@ -0,0 +1,29 @@
Welcome to LibBotUniversal
=============================
LibBotUniversal (AKA libbot) simplifies development and maintenance of different chatbots.
Getting started
*****************************
There are a few pages to take a look at if you're just getting started.
* Installing the library
* Creating your first bot
* Examples of different bots
Getting help
*****************************
If you are looking for help or want to report a bug, we got you covered.
* Bug reports are being submitted on the repo's issue tracker.
Package
*****************************
.. toctree::
:maxdepth: 1
:caption: API reference
modules

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

7
docs/modules.rst Normal file
View File

@@ -0,0 +1,7 @@
libbot
======
.. toctree::
:maxdepth: 5
libbot

View File

@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools>=62.6", "wheel"] requires = ["setuptools>=77.0.3", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@@ -9,11 +9,11 @@ authors = [{ name = "Profitroll" }]
description = "Universal bot library with functions needed for basic Discord/Telegram bot development." description = "Universal bot library with functions needed for basic Discord/Telegram bot development."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
license = { text = "GPLv3" } license = "GPL-3.0"
license-files = ["LICENSE"]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
@@ -28,6 +28,7 @@ dependencies = { file = "requirements/_.txt" }
[tool.setuptools.dynamic.optional-dependencies] [tool.setuptools.dynamic.optional-dependencies]
dev = { file = "requirements/dev.txt" } dev = { file = "requirements/dev.txt" }
docs = { file = "requirements/docs.txt" }
pycord = { file = "requirements/pycord.txt" } pycord = { file = "requirements/pycord.txt" }
pyrogram = { file = "requirements/pyrogram.txt" } pyrogram = { file = "requirements/pyrogram.txt" }
speed = { file = "requirements/speed.txt" } speed = { file = "requirements/speed.txt" }

View File

@@ -1,2 +1,2 @@
aiofiles>=23.0.0 aiofiles>=23.0.0
typing-extensions~=4.13.0 typing-extensions~=4.14.0

View File

@@ -1,2 +1,2 @@
pymemcache~=4.0.0 pymemcache~=4.0.0
redis~=6.1.0 redis~=6.2.0

View File

@@ -1,12 +1,12 @@
black==25.1.0 black==25.1.0
build==1.2.2.post1 build==1.2.2.post1
isort==5.13.2 isort==5.13.2
mypy==1.15.0 mypy==1.16.1
pylint==3.3.7 pylint==3.3.7
pytest-asyncio==0.26.0 pytest-asyncio==1.0.0
pytest-cov==6.1.1 pytest-cov==6.2.1
pytest==8.3.5 pytest==8.4.1
tox==4.26.0 tox==4.27.0
twine==6.1.0 twine==6.1.0
types-aiofiles==24.1.0.20250516 types-aiofiles==24.1.0.20250708
types-ujson==5.10.0.20250326 types-ujson==5.10.0.20250326

3
requirements/docs.txt Normal file
View File

@@ -0,0 +1,3 @@
furo==2024.8.6
sphinx==8.1.3
sphinx_copybutton==0.5.2

View File

@@ -1,4 +1,4 @@
__version__ = "4.2.0" __version__ = "4.3.0"
__license__ = "GPL3" __license__ = "GPL3"
__author__ = "Profitroll" __author__ = "Profitroll"

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Dict from typing import Any, Dict, Optional
import pymemcache import pymemcache
import redis import redis
@@ -27,16 +27,16 @@ class Cache(ABC):
pass pass
@abstractmethod @abstractmethod
def set_json(self, key: str, value: Any) -> None: def set_json(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
# TODO This method must also carry out ObjectId conversion! # TODO This method must also carry out ObjectId conversion!
pass pass
@abstractmethod @abstractmethod
def set_string(self, key: str, value: str) -> None: def set_string(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None:
pass pass
@abstractmethod @abstractmethod
def set_object(self, key: str, value: Any) -> None: def set_object(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
pass pass
@abstractmethod @abstractmethod

View File

@@ -13,20 +13,23 @@ logger: Logger = logging.getLogger(__name__)
class CacheMemcached(Cache): class CacheMemcached(Cache):
client: Client client: Client
def __init__(self, client: Client, prefix: Optional[str] = None): def __init__(
self, client: Client, prefix: Optional[str] = None, default_ttl_seconds: Optional[int] = None
) -> None:
self.client: Client = client self.client: Client = client
self.prefix: str | None = prefix self.prefix: str | None = prefix
self.default_ttl_seconds: int = default_ttl_seconds if default_ttl_seconds is not None else 0
logger.info("Initialized Memcached for caching") logger.info("Initialized Memcached for caching")
@classmethod @classmethod
def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None) -> "CacheMemcached": def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None, default_ttl_seconds: Optional[int] = None) -> "CacheMemcached":
if "uri" not in engine_config: if "uri" not in engine_config:
raise KeyError( raise KeyError(
"Cache configuration is invalid. Please check if all keys are set (engine: memcached)" "Cache configuration is invalid. Please check if all keys are set (engine: memcached)"
) )
return cls(Client(engine_config["uri"], default_noreply=True), prefix=prefix) return cls(Client(engine_config["uri"], default_noreply=True, default_ttl_seconds=default_ttl_seconds), prefix=prefix)
def _get_prefixed_key(self, key: str) -> str: def _get_prefixed_key(self, key: str) -> str:
return key if self.prefix is None else f"{self.prefix}_{key}" return key if self.prefix is None else f"{self.prefix}_{key}"
@@ -69,28 +72,34 @@ class CacheMemcached(Cache):
def get_object(self, key: str) -> Any | None: def get_object(self, key: str) -> Any | None:
raise NotImplementedError() raise NotImplementedError()
def set_json(self, key: str, value: Any) -> None: def set_json(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
key = self._get_prefixed_key(key) key = self._get_prefixed_key(key)
try: try:
self.client.set(key, _json_to_string(value)) self.client.set(
key,
_json_to_string(value),
expire=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds,
)
logger.debug("Set json cache key '%s'", key) logger.debug("Set json cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set json cache key '%s' due to: %s", key, exc) logger.error("Could not set json cache key '%s' due to: %s", key, exc)
return None return None
def set_string(self, key: str, value: str) -> None: def set_string(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None:
key = self._get_prefixed_key(key) key = self._get_prefixed_key(key)
try: try:
self.client.set(key, value) self.client.set(
key, value, expire=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds
)
logger.debug("Set string cache key '%s'", key) logger.debug("Set string cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set string cache key '%s' due to: %s", key, exc) logger.error("Could not set string cache key '%s' due to: %s", key, exc)
return None return None
# TODO Implement binary serialization # TODO Implement binary serialization
def set_object(self, key: str, value: Any) -> None: def set_object(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
raise NotImplementedError() raise NotImplementedError()
def delete(self, key: str) -> None: def delete(self, key: str) -> None:

View File

@@ -5,7 +5,7 @@ from typing import Dict, Any, Optional
from redis import Redis from redis import Redis
from .cache import Cache from .cache import Cache
from ..utils._objects import _string_to_json, _json_to_string from ..utils._objects import _json_to_string, _string_to_json
logger: Logger = logging.getLogger(__name__) logger: Logger = logging.getLogger(__name__)
@@ -13,20 +13,23 @@ logger: Logger = logging.getLogger(__name__)
class CacheRedis(Cache): class CacheRedis(Cache):
client: Redis client: Redis
def __init__(self, client: Redis, prefix: Optional[str] = None): def __init__(
self, client: Redis, prefix: Optional[str] = None, default_ttl_seconds: Optional[int] = None
) -> None:
self.client: Redis = client self.client: Redis = client
self.prefix: str | None = prefix self.prefix: str | None = prefix
self.default_ttl_seconds: int | None = default_ttl_seconds
logger.info("Initialized Redis for caching") logger.info("Initialized Redis for caching")
@classmethod @classmethod
def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None) -> Any: def from_config(cls, engine_config: Dict[str, Any], prefix: Optional[str] = None, default_ttl_seconds: Optional[int] = None) -> Any:
if "uri" not in engine_config: if "uri" not in engine_config:
raise KeyError( raise KeyError(
"Cache configuration is invalid. Please check if all keys are set (engine: memcached)" "Cache configuration is invalid. Please check if all keys are set (engine: memcached)"
) )
return cls(Redis.from_url(engine_config["uri"]), prefix=prefix) return cls(Redis.from_url(engine_config["uri"]), prefix=prefix, default_ttl_seconds=default_ttl_seconds)
def _get_prefixed_key(self, key: str) -> str: def _get_prefixed_key(self, key: str) -> str:
return key if self.prefix is None else f"{self.prefix}_{key}" return key if self.prefix is None else f"{self.prefix}_{key}"
@@ -69,28 +72,32 @@ class CacheRedis(Cache):
def get_object(self, key: str) -> Any | None: def get_object(self, key: str) -> Any | None:
raise NotImplementedError() raise NotImplementedError()
def set_json(self, key: str, value: Any) -> None: def set_json(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
key = self._get_prefixed_key(key) key = self._get_prefixed_key(key)
try: try:
self.client.set(key, _json_to_string(value)) self.client.set(
key,
_json_to_string(value),
ex=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds,
)
logger.debug("Set json cache key '%s'", key) logger.debug("Set json cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set json cache key '%s' due to: %s", key, exc) logger.error("Could not set json cache key '%s' due to: %s", key, exc)
return None return None
def set_string(self, key: str, value: str) -> None: def set_string(self, key: str, value: str, ttl_seconds: Optional[int] = None) -> None:
key = self._get_prefixed_key(key) key = self._get_prefixed_key(key)
try: try:
self.client.set(key, value) self.client.set(key, value, ex=self.default_ttl_seconds if ttl_seconds is None else ttl_seconds)
logger.debug("Set string cache key '%s'", key) logger.debug("Set string cache key '%s'", key)
except Exception as exc: except Exception as exc:
logger.error("Could not set string cache key '%s' due to: %s", key, exc) logger.error("Could not set string cache key '%s' due to: %s", key, exc)
return None return None
# TODO Implement binary serialization # TODO Implement binary serialization
def set_object(self, key: str, value: Any) -> None: def set_object(self, key: str, value: Any, ttl_seconds: Optional[int] = None) -> None:
raise NotImplementedError() raise NotImplementedError()
def delete(self, key: str) -> None: def delete(self, key: str) -> None:

View File

@@ -7,7 +7,19 @@ def create_cache_client(
config: Dict[str, Any], config: Dict[str, Any],
engine: Literal["memcached", "redis"] | None = None, engine: Literal["memcached", "redis"] | None = None,
prefix: Optional[str] = None, prefix: Optional[str] = None,
default_ttl_seconds: Optional[int] = None,
) -> CacheMemcached | CacheRedis: ) -> CacheMemcached | CacheRedis:
"""Create a cache client of a provided type.
Args:
config (Dict[str, Any]): Cache client configuration.
engine (Literal["memcached", "redis"] | None): Cache engine to use. Defaults to None.
prefix (:obj:`str`, optional): Prefix used for each key-value pair. Defaults to None (no prefix).
default_ttl_seconds (:obj:`int`, optional): Default TTL for values (in seconds). Defaults to None (does not expire).
Returns:
CacheMemcached | CacheRedis: Cache client.
"""
if engine not in ["memcached", "redis"] or engine is None: if engine not in ["memcached", "redis"] or engine is None:
raise KeyError(f"Incorrect cache engine provided. Expected 'memcached' or 'redis', got '{engine}'") raise KeyError(f"Incorrect cache engine provided. Expected 'memcached' or 'redis', got '{engine}'")
@@ -18,8 +30,8 @@ def create_cache_client(
match engine: match engine:
case "memcached": case "memcached":
return CacheMemcached.from_config(config["cache"][engine], prefix=prefix) return CacheMemcached.from_config(config["cache"][engine], prefix=prefix, default_ttl_seconds=default_ttl_seconds)
case "redis": case "redis":
return CacheRedis.from_config(config["cache"][engine], prefix=prefix) return CacheRedis.from_config(config["cache"][engine], prefix=prefix, default_ttl_seconds=default_ttl_seconds)
case _: case _:
raise KeyError(f"Cache implementation for the engine '{engine}' is not present.") raise KeyError(f"Cache implementation for the engine '{engine}' is not present.")

View File

@@ -4,8 +4,8 @@ from typing import Any, List, Optional
class ConfigKeyError(Exception): class ConfigKeyError(Exception):
"""Raised when config key is not found. """Raised when config key is not found.
### Attributes: Args:
* key (`str | List[str]`): Missing config key. key (str | List[str]): Missing config key
""" """
def __init__(self, key: str | List[str]) -> None: def __init__(self, key: str | List[str]) -> None:
@@ -21,9 +21,9 @@ class ConfigKeyError(Exception):
class ConfigValueError(Exception): class ConfigValueError(Exception):
"""Raised when config key's value is invalid. """Raised when config key's value is invalid.
### Attributes: Args:
* key (`str | List[str]`): Invalid config key. key (str | List[str]): Invalid config key
* value (`Optional[Any]`): Key's correct value. value (Optional[Any]): Key's correct value
""" """
def __init__(self, key: str | List[str], value: Optional[Any] = None) -> None: def __init__(self, key: str | List[str], value: Optional[Any] = None) -> None:

View File

@@ -18,16 +18,16 @@ def _(
locale: str | None = "en", locale: str | None = "en",
locales_root: str | Path = Path("locale"), locales_root: str | Path = Path("locale"),
) -> Any: ) -> Any:
"""Get value of locale string """Get value of the locale string.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
* locale (`str | None`): Locale to looked up in. Defaults to `"en"`. locale (str | None): Locale to be looked up in. Defaults to `"en"`
* locales_root (`str | Path`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. locales_root (str | Path, optional):Folder where locales are located. Defaults to `Path("locale")`
### Returns: Returns:
* `Any`: Value of provided locale key. Is usually `str`, `Dict[str, Any]` or `List[Any]` Any: Value of provided locale key. Is usually `str`, `Dict[str, Any]` or `List[Any]`
""" """
if locale is None: if locale is None:
locale: str = config_get("locale") locale: str = config_get("locale")
@@ -58,16 +58,16 @@ async def _(
locale: str | None = "en", locale: str | None = "en",
locales_root: str | Path = Path("locale"), locales_root: str | Path = Path("locale"),
) -> Any: ) -> Any:
"""Get value of locale string """Get value of the locale string.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
* locale (`str | None`): Locale to looked up in. Defaults to `"en"`. locale (str | None): Locale to be looked up in. Defaults to `"en"`
* locales_root (`str | Path`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. locales_root (str | Path, optional):Folder where locales are located. Defaults to `Path("locale")`
### Returns: Returns:
* `Any`: Value of provided locale key. Is usually `str`, `Dict[str, Any]` or `List[Any]` Any: Value of provided locale key. Is usually `str`, `Dict[str, Any]` or `List[Any]`
""" """
locale: str = config_get("locale") if locale is None else locale locale: str = config_get("locale") if locale is None else locale
@@ -94,17 +94,16 @@ async def _(
@asyncable @asyncable
def in_all_locales(key: str, *args: str, locales_root: str | Path = Path("locale")) -> List[Any]: def in_all_locales(key: str, *args: str, locales_root: str | Path = Path("locale")) -> List[Any]:
"""Get value of the provided key and path in all available locales """Get value of the provided key and path in all available locales.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
* locales_root (`str | Path`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. locales_root (str | Path, optional): Folder where locales are located. Defaults to `Path("locale")`
### Returns: Returns:
* `List[Any]`: List of values in all locales List[Any]: List of values in all locales
""" """
output: List[Any] = [] output: List[Any] = []
for locale in _get_valid_locales(locales_root): for locale in _get_valid_locales(locales_root):
@@ -128,15 +127,15 @@ def in_all_locales(key: str, *args: str, locales_root: str | Path = Path("locale
@in_all_locales.asynchronous @in_all_locales.asynchronous
async def in_all_locales(key: str, *args: str, locales_root: str | Path = Path("locale")) -> List[Any]: async def in_all_locales(key: str, *args: str, locales_root: str | Path = Path("locale")) -> List[Any]:
"""Get value of the provided key and path in all available locales """Get value of the provided key and path in all available locales.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
* locales_root (`str | Path`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. locales_root (str | Path, optional): Folder where locales are located. Defaults to `Path("locale")`
### Returns: Returns:
* `List[Any]`: List of values in all locales List[Any]: List of values in all locales
""" """
output: List[Any] = [] output: List[Any] = []
@@ -161,20 +160,17 @@ async def in_all_locales(key: str, *args: str, locales_root: str | Path = Path("
@asyncable @asyncable
def in_every_locale( def in_every_locale(key: str, *args: str, locales_root: str | Path = Path("locale")) -> Dict[str, Any]:
key: str, *args: str, locales_root: str | Path = Path("locale") """Get value of the provided key and path in every available locale with locale tag.
) -> Dict[str, Any]:
"""Get value of the provided key and path in every available locale with locale tag
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
* locales_root (`str | Path`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. locales_root (str | Path, optional): Folder where locales are located. Defaults to `Path("locale")`
### Returns: Returns:
* `Dict[str, Any]`: Locale is a key, and it's value from locale file is a value Dict[str, Any]: Locale is a key, and it's value from locale file is a value
""" """
output: Dict[str, Any] = {} output: Dict[str, Any] = {}
for locale in _get_valid_locales(locales_root): for locale in _get_valid_locales(locales_root):
@@ -200,15 +196,15 @@ def in_every_locale(
async def in_every_locale( async def in_every_locale(
key: str, *args: str, locales_root: str | Path = Path("locale") key: str, *args: str, locales_root: str | Path = Path("locale")
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get value of the provided key and path in every available locale with locale tag """Get value of the provided key and path in every available locale with locale tag.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
* locales_root (`str | Path`, *optional*): Folder where locales are located. Defaults to `Path("locale")`. locales_root (str | Path, optional): Folder where locales are located. Defaults to `Path("locale")`
### Returns: Returns:
* `Dict[str, Any]`: Locale is a key, and it's value from locale file is a value Dict[str, Any]: Locale is a key, and it's value from locale file is a value
""" """
output: Dict[str, Any] = {} output: Dict[str, Any] = {}

View File

@@ -14,6 +14,11 @@ class BotLocale:
default_locale: str | None = "en", default_locale: str | None = "en",
locales_root: str | Path = Path("locale"), locales_root: str | Path = Path("locale"),
) -> None: ) -> None:
"""
Args:
default_locale (str | None): Default bot's locale. Defaults to `"en"`
locales_root (str | Path): Folder where locales are located. Defaults to `Path("locale")`
"""
if isinstance(locales_root, str): if isinstance(locales_root, str):
locales_root = Path(locales_root) locales_root = Path(locales_root)
elif not isinstance(locales_root, Path): elif not isinstance(locales_root, Path):
@@ -30,15 +35,15 @@ class BotLocale:
self.locales[locale] = json_read(Path(f"{locales_root}/{locale}.json")) self.locales[locale] = json_read(Path(f"{locales_root}/{locale}.json"))
def _(self, key: str, *args: str, locale: str | None = None) -> Any: def _(self, key: str, *args: str, locale: str | None = None) -> Any:
"""Get value of locale string """Get value of locale string.
### Args: Args:
* key (`str`): The last key of the locale's keys path key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]` *args (str): Path to key like: `dict[args][key]`
* locale (`str | None`, *optional*): Locale to looked up in. Defaults to config's `"locale"` value locale (str | None, optional): Locale to be looked up in. Defaults to config's `"locale"` value
### Returns: Returns:
* `Any`: Value of provided locale key. Is usually `str`, `Dict[str, Any]` or `List[Any]` Any: Value of provided locale key. Is usually `str`, `Dict[str, Any]` or `List[Any]`
""" """
if locale is None: if locale is None:
locale: str = self.default locale: str = self.default
@@ -64,14 +69,14 @@ class BotLocale:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"' return f'⚠️ Locale in config is invalid: could not get "{key}" in {args} from locale "{locale}"'
def in_all_locales(self, key: str, *args: str) -> List[Any]: def in_all_locales(self, key: str, *args: str) -> List[Any]:
"""Get value of the provided key and path in all available locales """Get value of the provided key and path in all available locales.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
### Returns: Returns:
* `List[Any]`: List of values in all locales List[Any]: List of values in all locales
""" """
output: List[Any] = [] output: List[Any] = []
@@ -94,14 +99,14 @@ class BotLocale:
return output return output
def in_every_locale(self, key: str, *args: str) -> Dict[str, Any]: def in_every_locale(self, key: str, *args: str) -> Dict[str, Any]:
"""Get value of the provided key and path in every available locale with locale tag """Get value of the provided key and path in every available locale with locale tag.
### Args: Args:
* key (`str`): The last key of the locale's keys path. key (str): The last key of the locale's keys path
* *args (`str`): Path to key like: `dict[args][key]`. *args (str): Path to key like: `dict[args][key]`
### Returns: Returns:
* `Dict[str, Any]`: Locale is a key, and it's value from locale file is a value Dict[str, Any]: Locale is a key, and it's value from locale file is a value
""" """
output: Dict[str, Any] = {} output: Dict[str, Any] = {}

View File

@@ -19,16 +19,27 @@ logger: Logger = logging.getLogger(__name__)
class PycordBot(Bot): class PycordBot(Bot):
# TODO Write a docstring
@override @override
def __init__( def __init__(
self, self,
*args, *args,
config: Dict[str, Any] | None = None, config: Dict[str, Any] | None = None,
config_path: str | Path = Path("config.json"), config_path: str | Path = Path("config.json"),
locales_root: str | Path | None = None, locales_root: str | Path | None = None,
scheduler: AsyncIOScheduler | BackgroundScheduler | None = None, scheduler: AsyncIOScheduler | BackgroundScheduler | None = None,
**kwargs, **kwargs,
): ):
"""
Args:
*args:
config:
config_path:
locales_root:
scheduler:
**kwargs:
"""
self.config: Dict[str, Any] = config if config is not None else json_read(config_path) self.config: Dict[str, Any] = config if config is not None else json_read(config_path)
super().__init__( super().__init__(
@@ -51,15 +62,30 @@ class PycordBot(Bot):
self.scheduler: AsyncIOScheduler | BackgroundScheduler | None = scheduler self.scheduler: AsyncIOScheduler | BackgroundScheduler | None = scheduler
# TODO Write a docstring
@override @override
async def start(self, token: str, reconnect: bool = True, scheduler_start: bool = True) -> None: async def start(self, token: str, reconnect: bool = True, scheduler_start: bool = True) -> None:
"""
Args:
token:
reconnect:
scheduler_start:
"""
if self.scheduler is not None and scheduler_start: if self.scheduler is not None and scheduler_start:
self.scheduler.start() self.scheduler.start()
await super().start(token, reconnect=reconnect) await super().start(token, reconnect=reconnect)
# TODO Write a docstring
@override @override
async def close(self, scheduler_shutdown: bool = True, scheduler_wait: bool = True) -> None: async def close(self, scheduler_shutdown: bool = True, scheduler_wait: bool = True) -> None:
"""
Args:
scheduler_shutdown:
scheduler_wait:
"""
if self.scheduler is not None and scheduler_shutdown: if self.scheduler is not None and scheduler_shutdown:
self.scheduler.shutdown(scheduler_wait) self.scheduler.shutdown(scheduler_wait)

View File

@@ -16,20 +16,24 @@ def _hex_from_int(color_int: int) -> str:
def color_from_hex(hex_string: str) -> Colour: def color_from_hex(hex_string: str) -> Colour:
"""Convert valid hexadecimal string to discord.Colour. """Convert valid hexadecimal string to :class:`discord.Colour`.
:param hex_string: Hexadecimal string to convert into Colour object Args:
:type hex_string: str hex_string (str): Hexadecimal string to convert into :class:`discord.Colour` object
:return: Colour object
Returns:
Colour: :class:`discord.Colour` object
""" """
return Colour(_int_from_hex(hex_string)) return Colour(_int_from_hex(hex_string))
def hex_from_color(color: Colour) -> str: def hex_from_color(color: Colour) -> str:
"""Convert discord.Colour to hexadecimal string. """Convert :class:`discord.Colour` to hexadecimal string.
:param color: Colour object to convert into the string Args:
:type color: Colour color (Colour): :class:`discord.Colour` object to convert into the string
:return: Hexadecimal string in #XXXXXX format
Returns:
str: Hexadecimal string in #XXXXXX format
""" """
return _hex_from_int(color.value) return _hex_from_int(color.value)

View File

@@ -6,7 +6,7 @@ from logging import Logger
from os import cpu_count, getpid from os import cpu_count, getpid
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import Any, Dict, List from typing import Any, Dict, List, Optional
from typing_extensions import override from typing_extensions import override
@@ -46,27 +46,28 @@ logger: Logger = logging.getLogger(__name__)
class PyroClient(Client): class PyroClient(Client):
# TODO Write a docstring
@override @override
def __init__( def __init__(
self, self,
name: str = "bot_client", name: str = "bot_client",
owner: int | None = None, owner: int | None = None,
config: Dict[str, Any] | None = None, config: Dict[str, Any] | None = None,
config_path: str | Path = Path("config.json"), config_path: str | Path = Path("config.json"),
api_id: int | None = None, api_id: int | None = None,
api_hash: str | None = None, api_hash: str | None = None,
bot_token: str | None = None, bot_token: str | None = None,
workers: int = min(32, cpu_count() + 4), workers: int = min(32, cpu_count() + 4),
locales_root: str | Path | None = None, locales_root: str | Path | None = None,
plugins_root: str = "plugins", plugins_root: str = "plugins",
plugins_exclude: List[str] | None = None, plugins_exclude: List[str] | None = None,
sleep_threshold: int = 120, sleep_threshold: int = 120,
max_concurrent_transmissions: int = 1, max_concurrent_transmissions: int = 1,
commands_source: Dict[str, dict] | None = None, commands_source: Dict[str, dict] | None = None,
scoped_commands: bool | None = None, scoped_commands: bool | None = None,
i18n_bot_info: bool = False, i18n_bot_info: bool = False,
scheduler: AsyncIOScheduler | BackgroundScheduler | None = None, scheduler: AsyncIOScheduler | BackgroundScheduler | None = None,
**kwargs, **kwargs,
): ):
self.config: Dict[str, Any] = config if config is not None else json_read(config_path) self.config: Dict[str, Any] = config if config is not None else json_read(config_path)
@@ -119,6 +120,12 @@ class PyroClient(Client):
@override @override
async def start(self, register_commands: bool = True, scheduler_start: bool = True) -> None: async def start(self, register_commands: bool = True, scheduler_start: bool = True) -> None:
"""Start the bot and it's services.
Args:
register_commands (bool, optional): Register commands on start. Defaults to `True`
scheduler_start (bool, optional): Start the scheduler on start. Defaults to `True`
"""
await super().start() await super().start()
self.start_time = time() self.start_time = time()
@@ -209,8 +216,15 @@ class PyroClient(Client):
@override @override
async def stop( async def stop(
self, exit_completely: bool = True, scheduler_shutdown: bool = True, scheduler_wait: bool = True self, exit_completely: bool = True, scheduler_shutdown: bool = True, scheduler_wait: bool = True
) -> None: ) -> None:
"""Stop the bot and it's services.
Args:
exit_completely (bool, optional): Exit the program. Defaults to `True`
scheduler_shutdown (bool): Shutdown the scheduler. Defaults to `True`
scheduler_wait (bool): Wait for tasks to finish. Defaults to `True`
"""
try: try:
await self.send_message( await self.send_message(
chat_id=( chat_id=(
@@ -237,10 +251,10 @@ class PyroClient(Client):
raise SystemExit("Bot has been shut down, this is not an application error!") from exc raise SystemExit("Bot has been shut down, this is not an application error!") from exc
async def collect_commands(self) -> List[CommandSet] | None: async def collect_commands(self) -> List[CommandSet] | None:
"""Gather list of the bot's commands """Gather list of the bot's commands.
### Returns: Returns:
* `List[CommandSet]`: List of the commands' sets. List[CommandSet]: List of the commands' sets
""" """
command_sets = None command_sets = None
@@ -308,7 +322,7 @@ class PyroClient(Client):
# in it, if there are any. Then adds them to self.commands # in it, if there are any. Then adds them to self.commands
for handler in self.dispatcher.groups[0]: for handler in self.dispatcher.groups[0]:
if isinstance(handler, MessageHandler) and ( if isinstance(handler, MessageHandler) and (
hasattr(handler.filters, "base") or hasattr(handler.filters, "other") hasattr(handler.filters, "base") or hasattr(handler.filters, "other")
): ):
for entry in [handler.filters.base, handler.filters.other]: for entry in [handler.filters.base, handler.filters.other]:
if hasattr(entry, "commands"): if hasattr(entry, "commands"):
@@ -319,13 +333,13 @@ class PyroClient(Client):
return command_sets return command_sets
def add_command( def add_command(
self, self,
command: str, command: str,
) -> None: ) -> None:
"""Add command to the bot's internal commands list """Add command to the bot's internal commands list.
### Args: Args:
* command (`str`) command (str): Command's name
""" """
self.commands.append( self.commands.append(
PyroCommand( PyroCommand(
@@ -338,9 +352,12 @@ class PyroClient(Client):
command, command,
) )
async def register_commands(self, command_sets: List[CommandSet] | None = None) -> None: async def register_commands(self, command_sets: Optional[List[CommandSet]] = None) -> None:
"""Register commands stored in bot's 'commands' attribute""" """Register commands stored in bot's 'commands' attribute.
Args:
command_sets (List[CommandSet], optional): List of command sets. Commands will be parsed from bot's 'commands' attribute if not provided
"""
if command_sets is None: if command_sets is None:
commands = [ commands = [
BotCommand(command=command.command, description=command.description) BotCommand(command=command.command, description=command.description)
@@ -365,9 +382,12 @@ class PyroClient(Client):
language_code=command_set.language_code, language_code=command_set.language_code,
) )
async def remove_commands(self, command_sets: List[CommandSet] | None = None) -> None: async def remove_commands(self, command_sets: Optional[List[CommandSet]] = None) -> None:
"""Remove commands stored in bot's 'commands' attribute""" """Remove commands stored in bot's 'commands' attribute.
Args:
command_sets (List[CommandSet], optional): List of command sets. Commands with scope :class:`pyrogram.types.BotCommandScopeDefault` will be removed if not provided
"""
if command_sets is None: if command_sets is None:
logger.info("Removing commands with a default scope 'BotCommandScopeDefault'") logger.info("Removing commands with a default scope 'BotCommandScopeDefault'")
await self.delete_bot_commands(BotCommandScopeDefault()) await self.delete_bot_commands(BotCommandScopeDefault())

View File

@@ -15,34 +15,24 @@ DEFAULT_CONFIG_LOCATION: str = "config.json"
@asyncable @asyncable
def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION) -> Any: def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION) -> Any:
"""Get a value of the config key by its path provided """Get a value of the config key by its path provided.
For example, `foo.bar.key` has a path of `"foo", "bar"` and the key `"key"` For example, `foo.bar.key` has a path of `"foo", "bar"` and the key `"key"`.
### Args: Args:
* key (`str`): Key that contains the value key (str): Key that contains the value
* *path (`str`): Path to the key that contains the value (pass *[] or don't pass anything at all to get on the top/root level) *path (str): Path to the key that contains the value (pass *[] or don't pass anything at all to get on the top/root level)
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"` config_file (str | Path, optional): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
### Returns: Returns:
* `Any`: Key's value Any: Key's value
### Example: Example:
Get the "salary" of "Pete" from this JSON structure: Get the "salary" of "Pete" from this JSON structure: `{"users": {"Pete": {"salary": 10.0}}}`
```json
{
"users": {
"Pete": {
"salary": 10.0
}
}
}
```
This can be easily done with the following code: This can be easily done with the following code:
```python
import libbot >>> import libbot
salary = libbot.sync.config_get("salary", "users", "Pete") salary: float = libbot.sync.config_get("salary", "users", "Pete")
```
""" """
this_key: Dict[str, Any] = json_read(config_file) this_key: Dict[str, Any] = json_read(config_file)
@@ -54,34 +44,24 @@ def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CONFIG_LO
@config_get.asynchronous @config_get.asynchronous
async def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION) -> Any: async def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION) -> Any:
"""Get a value of the config key by its path provided """Get a value of the config key by its path provided.
For example, `foo.bar.key` has a path of `"foo", "bar"` and the key `"key"` For example, `foo.bar.key` has a path of `"foo", "bar"` and the key `"key"`.
### Args: Args:
* key (`str`): Key that contains the value key (str): Key that contains the value
* *path (`str`): Path to the key that contains the value (pass *[] or don't pass anything at all to get on the top/root level) *path (str): Path to the key that contains the value (pass *[] or don't pass anything at all to get on the top/root level)
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"` config_file (str | Path, optional): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"`
### Returns: Returns:
* `Any`: Key's value Any: Key's value
### Example: Example:
Get the "salary" of "Pete" from this JSON structure: Get the "salary" of "Pete" from this JSON structure: `{"users": {"Pete": {"salary": 10.0}}}`
```json
{
"users": {
"Pete": {
"salary": 10.0
}
}
}
```
This can be easily done with the following code: This can be easily done with the following code:
```python
import libbot >>> import libbot
salary = await libbot.config_get("salary", "users", "Pete") salary: float = libbot.sync.config_get("salary", "users", "Pete")
```
""" """
this_key: Dict[str, Any] = await json_read(config_file) this_key: Dict[str, Any] = await json_read(config_file)
@@ -93,16 +73,16 @@ async def config_get(key: str, *path: str, config_file: str | Path = DEFAULT_CON
@asyncable @asyncable
def config_set(key: str, value: Any, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION) -> None: def config_set(key: str, value: Any, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION) -> None:
"""Set config's key by its path to the value """Set config's key by its path to the value.
### Args: Args:
* key (`str`): Key that leads to the value key (str): Key that leads to the value.
* value (`Any`): Any JSON serializable data value (Any): Any JSON-serializable data.
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to set on the top/root level) *path (str): Path to the key of the target (pass *[] or don't pass anything at all to set on the top/root level).
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"` config_file (str | Path, optional): Path-like object or path as a string of a location of the config file. Defaults to "config.json".
### Raises: Raises:
* `KeyError`: Key is not found under path provided KeyError: Key was not found under the provided path.
""" """
json_write(nested_set(json_read(config_file), value, *(*path, key)), config_file) json_write(nested_set(json_read(config_file), value, *(*path, key)), config_file)
@@ -111,16 +91,16 @@ def config_set(key: str, value: Any, *path: str, config_file: str | Path = DEFAU
async def config_set( async def config_set(
key: str, value: Any, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION key: str, value: Any, *path: str, config_file: str | Path = DEFAULT_CONFIG_LOCATION
) -> None: ) -> None:
"""Set config's key by its path to the value """Set config's key by its path to the value.
### Args: Args:
* key (`str`): Key that leads to the value key (str): Key that leads to the value.
* value (`Any`): Any JSON serializable data value (Any): Any JSON-serializable data.
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to set on the top/root level) *path (str): Path to the key of the target (pass *[] or don't pass anything at all to set on the top/root level).
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"` config_file (str | Path, optional): Path-like object or path as a string of a location of the config file. Defaults to "config.json".
### Raises: Raises:
* `KeyError`: Key is not found under path provided KeyError: Key was not found under the provided path.
""" """
await json_write(nested_set(await json_read(config_file), value, *(*path, key)), config_file) await json_write(nested_set(await json_read(config_file), value, *(*path, key)), config_file)
@@ -132,16 +112,16 @@ def config_delete(
missing_ok: bool = False, missing_ok: bool = False,
config_file: str | Path = DEFAULT_CONFIG_LOCATION, config_file: str | Path = DEFAULT_CONFIG_LOCATION,
) -> None: ) -> None:
"""Set config's key by its path """Delete config's key by its path.
### Args: Args:
* key (`str`): Key to delete key (str): Key to delete.
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to delete on the top/root level) *path (str): Path to the key of the target (pass *[] or don't pass anything at all to delete on the top/root level)
* missing_ok (`bool`): Do not raise an exception if the key is missing. Defaults to `False` missing_ok (bool): Do not raise an exception if the key is missing. Defaults to False.
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"` config_file (str | Path, optional): Path-like object or path as a string of a location of the config file. Defaults to "config.json".
### Raises: Raises:
* `KeyError`: Key is not found under path provided and `missing_ok` is `False` KeyError: Key is not found under path provided and `missing_ok` is False.
""" """
config_data: Dict[str, Any] = json_read(config_file) config_data: Dict[str, Any] = json_read(config_file)
@@ -161,16 +141,16 @@ async def config_delete(
missing_ok: bool = False, missing_ok: bool = False,
config_file: str | Path = DEFAULT_CONFIG_LOCATION, config_file: str | Path = DEFAULT_CONFIG_LOCATION,
) -> None: ) -> None:
"""Set config's key by its path """Delete config's key by its path.
### Args: Args:
* key (`str`): Key to delete key (str): Key to delete.
* *path (`str`): Path to the key of the target (pass *[] or don't pass anything at all to delete on the top/root level) *path (str): Path to the key of the target (pass *[] or don't pass anything at all to delete on the top/root level)
* missing_ok (`bool`): Do not raise an exception if the key is missing. Defaults to `False` missing_ok (bool): Do not raise an exception if the key is missing. Defaults to False.
* config_file (`str | Path`, *optional*): Path-like object or path as a string of a location of the config file. Defaults to `"config.json"` config_file (str | Path, optional): Path-like object or path as a string of a location of the config file. Defaults to "config.json".
### Raises: Raises:
* `KeyError`: Key is not found under path provided and `missing_ok` is `False` KeyError: Key is not found under path provided and `missing_ok` is False.
""" """
config_data: Dict[str, Any] = await json_read(config_file) config_data: Dict[str, Any] = await json_read(config_file)

View File

@@ -14,13 +14,13 @@ except ImportError:
@asyncable @asyncable
def json_read(path: str | Path) -> Any: def json_read(path: str | Path) -> Any:
"""Read contents of a JSON file """Read contents of a JSON file and return it.
### Args: Args:
* path (`str | Path`): Path-like object or path as a string path (str | Path): Path-like object or path to the file as a string.
### Returns: Returns:
* `Any`: File contents Any: File contents.
""" """
with open(str(path), mode="r", encoding="utf-8") as f: with open(str(path), mode="r", encoding="utf-8") as f:
data = f.read() data = f.read()
@@ -30,13 +30,13 @@ def json_read(path: str | Path) -> Any:
@json_read.asynchronous @json_read.asynchronous
async def json_read(path: str | Path) -> Any: async def json_read(path: str | Path) -> Any:
"""Read contents of a JSON file """Read contents of a JSON file and return it.
### Args: Args:
* path (`str | Path`): Path-like object or path as a string path (str | Path): Path-like object or path to the file as a string.
### Returns: Returns:
* `Any`: File contents Any: File contents.
""" """
async with aiofiles.open(str(path), mode="r", encoding="utf-8") as f: async with aiofiles.open(str(path), mode="r", encoding="utf-8") as f:
data = await f.read() data = await f.read()
@@ -46,11 +46,11 @@ async def json_read(path: str | Path) -> Any:
@asyncable @asyncable
def json_write(data: Any, path: str | Path) -> None: def json_write(data: Any, path: str | Path) -> None:
"""Write contents to a JSON file """Write contents to a JSON file.
### Args: Args:
* data (`Any`): Contents to write. Must be a JSON serializable data (Any): Contents to write. Must be a JSON-serializable object.
* path (`str | Path`): Path-like object or path as a string of a destination path (str | Path): Path-like object or path to the file as a string.
""" """
with open(str(path), mode="w", encoding="utf-8") as f: with open(str(path), mode="w", encoding="utf-8") as f:
f.write( f.write(
@@ -62,11 +62,11 @@ def json_write(data: Any, path: str | Path) -> None:
@json_write.asynchronous @json_write.asynchronous
async def json_write(data: Any, path: str | Path) -> None: async def json_write(data: Any, path: str | Path) -> None:
"""Write contents to a JSON file """Write contents to a JSON file.
### Args: Args:
* data (`Any`): Contents to write. Must be a JSON serializable data (Any): Contents to write. Must be a JSON-serializable object.
* path (`str | Path`): Path-like object or path as a string of a destination path (str | Path): Path-like object or path to the file as a string.
""" """
async with aiofiles.open(str(path), mode="w", encoding="utf-8") as f: async with aiofiles.open(str(path), mode="w", encoding="utf-8") as f:
await f.write( await f.write(

View File

@@ -4,14 +4,14 @@ from typing import Callable
def supports_argument(func: Callable[..., Any], arg_name: str) -> bool: def supports_argument(func: Callable[..., Any], arg_name: str) -> bool:
"""Check whether a function has a specific argument """Check whether a function has a specific argument.
### Args: Args:
* func (`Callable[..., Any]`): Function to be inspected func (Callable[..., Any]): Function to be inspected.
* arg_name (`str`): Argument to be checked arg_name (str): Argument to be checked.
### Returns: Returns:
* `bool`: `True` if argument is supported and `False` if not bool: True if argument is supported and False if not.
""" """
if hasattr(func, "__code__"): if hasattr(func, "__code__"):
return arg_name in inspect.signature(func).parameters return arg_name in inspect.signature(func).parameters
@@ -29,17 +29,17 @@ def nested_set(
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Set the key by its path to the value """Set the key by its path to the value
### Args: Args:
* target (`Dict[str, Any]`): Dictionary to perform modifications on target (Dict[str, Any]): Dictionary to perform the modification on.
* value (`Any`): Any data value (Any): New value.
* *path (`str`): Path to the key of the target *path (str): Path to the key.
* create_missing (`bool`, *optional*): Create keys on the way if they're missing. Defaults to `True` create_missing (:obj:`bool`, optional): Create keys on the way if they're missing. Defaults to True.
### Raises: Raises:
* `KeyError`: Key is not found under path provided KeyError: Key is not found under the provided path.
### Returns: Returns:
* `Dict[str, Any]`: Changed dictionary Dict[str, Any]: Modified dictionary.
""" """
target_copy: Dict[str, Any] = target target_copy: Dict[str, Any] = target
@@ -60,16 +60,16 @@ def nested_set(
def nested_delete(target: Dict[str, Any], *path: str) -> Dict[str, Any]: def nested_delete(target: Dict[str, Any], *path: str) -> Dict[str, Any]:
"""Delete the key by its path """Delete the key by its path.
### Args: Args:
* target (`Dict[str, Any]`): Dictionary to perform modifications on target (Dict[str, Any]): Dictionary to perform the modification on.
### Raises: Raises:
* `KeyError`: Key is not found under path provided KeyError: Key is not found under the provided path.
### Returns: Returns:
`Dict[str, Any]`: Changed dictionary Dict[str, Any]: Modified dictionary.
""" """
target_copy: Dict[str, Any] = target target_copy: Dict[str, Any] = target