Skip to content

Commit

Permalink
add docs for v0.12.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Sidnev Nikolaj committed Aug 12, 2019
1 parent e204431 commit ae976b7
Show file tree
Hide file tree
Showing 16 changed files with 383 additions and 50 deletions.
3 changes: 2 additions & 1 deletion botx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from .bots import AsyncBot as Bot
from .collector import HandlersCollector
from .core import BotXException
from .core import BotXDependencyFailure, BotXException
from .dependencies import Depends
from .models import (
CTS,
Expand Down Expand Up @@ -41,6 +41,7 @@
logger.disable("botx")

__all__ = (
"BotXDependencyFailure",
"Depends",
"Bot",
"HandlersCollector",
Expand Down
50 changes: 26 additions & 24 deletions botx/clients.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import abc
from typing import Any, Awaitable, Dict, Optional
from typing import Any, Awaitable, Callable, Dict, Optional
from uuid import UUID

from httpx import AsyncClient
Expand Down Expand Up @@ -61,32 +61,31 @@ def send_notification(


class AsyncBotXClient(BaseBotXClient):
client: AsyncClient

def __init__(self) -> None:
self.client = AsyncClient()
asgi_app: Optional[Callable] = None

async def send_file(
self, credentials: SendingCredentials, payload: SendingPayload
) -> None:
assert payload.file, "payload should include File object"

resp = await self.client.post(
self._file_url.format(host=credentials.host),
data=BotXFilePayload.from_orm(credentials).dict(),
files={"file": payload.file.file},
)
async with AsyncClient(app=self.asgi_app) as client:
resp = await client.post(
self._file_url.format(host=credentials.host),
data=BotXFilePayload.from_orm(credentials).dict(),
files={"file": payload.file.file},
)
if check_api_error(resp):
raise BotXException(
"unable to send file to BotX API",
data=get_data_for_api_error(credentials, resp),
)

async def obtain_token(self, host: str, bot_id: UUID, signature: str) -> Any:
resp = await self.client.get(
self._token_url.format(host=host, bot_id=bot_id),
params=BotXTokenRequestParams(signature=signature).dict(),
)
async with AsyncClient(app=self.asgi_app) as client:
resp = await client.get(
self._token_url.format(host=host, bot_id=bot_id),
params=BotXTokenRequestParams(signature=signature).dict(),
)
if check_api_error(resp):
raise BotXException(
"unable to obtain token from BotX API",
Expand Down Expand Up @@ -114,11 +113,13 @@ async def send_command_result(
file=payload.file,
opts=BotXPayloadOptions(notification_opts=payload.options.notifications),
)
resp = await self.client.post(
self._command_url.format(host=credentials.host),
json=command_result.dict(),
headers=get_headers(credentials.token),
)

async with AsyncClient(app=self.asgi_app) as client:
resp = await client.post(
self._command_url.format(host=credentials.host),
json=command_result.dict(),
headers=get_headers(credentials.token),
)
if check_api_error(resp):
raise BotXException(
"unable to send command result to BotX API",
Expand All @@ -143,11 +144,12 @@ async def send_notification(
file=payload.file,
opts=BotXPayloadOptions(notification_opts=payload.options.notifications),
)
resp = await self.client.post(
self._notification_url.format(host=credentials.host),
json=notification.dict(),
headers=get_headers(credentials.token),
)
async with AsyncClient(app=self.asgi_app) as client:
resp = await client.post(
self._notification_url.format(host=credentials.host),
json=notification.dict(),
headers=get_headers(credentials.token),
)
if check_api_error(resp):
raise BotXException(
"unable to send notification to BotX API",
Expand Down
4 changes: 4 additions & 0 deletions botx/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def __init__(self, message: str = "", data: Optional[Dict[str, Any]] = None):
super().__init__(msg)


class BotXDependencyFailure(Exception):
pass


@dataclass
class BotXEndpoint:
method: str
Expand Down
3 changes: 3 additions & 0 deletions botx/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from loguru import logger

from .core import BotXDependencyFailure
from .dependencies import solve_dependencies
from .helpers import call_function_as_coroutine
from .models import CommandCallback, Dependency, Message
Expand Down Expand Up @@ -58,6 +59,8 @@ async def execute_callback_with_exception_catching(
await call_function_as_coroutine(
callback.callback, *callback.args, **callback.kwargs, **callback_deps
)
except BotXDependencyFailure:
pass
except Exception as exc:
catcher_res = await _handle_exception(exceptions_map, exc, message, bot)
if not catcher_res:
Expand Down
66 changes: 66 additions & 0 deletions docs/development/dependencies-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
`pybotx` has an dependency injection mechanism heavily inspired by [`FastAPI`](https://fastapi.tiangolo.com/tutorial/dependencies/first-steps/).

## Usage

First, create a function that will execute some logic. It can be a coroutine or a simple function.
Then write a handler for bot that will use this dependency:

```python3 hl_lines="7"
from botx import Bot, Message, Depends
...
def get_user_huid(message: Message) -> UUID:
return message.user_huid

@bot.handler
async def handler(user_huid: UUID = Depends(get_user_huid)):
print(f"Message from {user_huid}")
```

## Dependencies with dependencies

Each of your dependencies function can contain parameters with other dependencies. And all this will be solved at the runtime:

```python3 hl_lines="6"
from botx import Bot, Message, Depends
...
def get_user_huid(message: Message) -> UUID:
return message.user_huid

async def get_user(user_huid: UUID = Depends(get_user_huid)) -> User:
return await get_user_by_huid(user_huid)

@bot.handler
def handler(user: User = Depends(get_user)):
print(f"Message from {user.username}")
...
```

## Optional dependencies for bot and message

`Bot` and `Message` objects and special case of dependencies. If you put an annotation for them into your function then
this objects will be passed inside. It can be useful if you write something like authentication dependency:

```python3 hl_lines="3 7 9x"
from botx import Bot, Message, BotXDependecyFailure
...
def authenticate_user(message: Message, bot: Bot) -> None:
...
if not user.authenticated:
bot.answer_message("You should login first", message)
raise BotXDependencyFailure

@bot.handler(dependecies=[authenticate_user])
async def handler():
pass
...
```

## Background dependencies


If you define a list of callable objects in the initialization of `HandlersColletor` or in `HandlersCollector.handler`,
then these dependencies will be processed as background dependencies.
They will be executed before the handler and the dependencies of this handler in the following order:

* Dependencies defined in the `HandlersCollector` init.
* Dependencies defined in the handler decorator.
5 changes: 3 additions & 2 deletions docs/development/handlers-collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Let's divide these commands in a following way:
### `HandlersCollector`

`HandlersCollector` is a class that can collect registered handlers in itself and then transfer them to bot.
In fact `Bot` and `AsyncBot` are subclasses of `HandlersCollector` so all methods available to `HandlersCollector` are also available to bots.
In fact `Bot` is subclass of `HandlersCollector` so all methods available to `HandlersCollector` are also available to bots.


Using `HandlersCollector` is quite simple:

1. Create an instance of the class.
2. Register your handlers, just like you do it for your bot.
3. Include registered handlers in your `Bot` or `AsyncBot` instance using the `.include_handlers` method.
3. Include registered handlers in your `Bot` instance using the `.include_handlers` method.

Here is an example.

Expand Down Expand Up @@ -78,6 +78,7 @@ But it also provides some additional key arguments that can be used to change th
* `commands: List[str, Pattern] = None` - list of command aliases that will also run handler execution.
* `use_as_default_handler: bool = False` - indicates that the handler will be used in the absence of other handlers for the command.
* `exclude_from_status: bool = False` - indicates that handler will not appear in the list of public commands.
* `dependencies: List[Callable] = None` - list of background dependencies that will be executed before handler.

* `HandlersCollector.regex_handler` - is a handler that accepts string as regex string and then will match incoming message body
with `command` argument or strings from `commands` List.
Expand Down
1 change: 1 addition & 0 deletions docs/development/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`pybotx` uses `loguru` internally to log things. To enable it, just import `logger` from `loguru` and call `logger.enable("botx")`
30 changes: 25 additions & 5 deletions docs/development/sending-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ async def my_handler(message: Message, bot: Bot):
await bot.reply(reply)
```

### Send file

```python3
from botx import SendingCredentials
...
@bot.handler
async def my_handler(message: Message, bot: Bot):
with open("file.txt") as f:
file = File.from_file(f)

await bot.send_file(
file.file,
SendingCredentials(
sync_id=message.sync_id, bot_id=message.bot_id, host=message.host
),
)
...
```

### Send file with message

!!! warning
Expand All @@ -52,17 +71,18 @@ To attach a file to your message, simply pass it to the `file` argument for tje
or use `ReplyMessage.add_file` if you use `Bot.reply`. This will create an instance of the `File` class that will be used to send result.
If you want to use the same file several times, you can create a `File` object manually and use the `File.file` property to reuse the data.

```Python3
```python3
@bot.handler
async def my_handler(message: Message, bot: Bot)
with open('file.txt') as f:
async def my_handler(message: Message, bot: Bot):
with open("file.txt") as f:
file = File.from_file(f)

await bot.answer_message("Your file", message, file=file.file)

reply = ReplyMessage.from_message("Your file (again)", message)
reply.add_file(file)
await bot.reply(reply)
...
```

### Add `Bubble` and `Keyboard`
Expand Down
21 changes: 20 additions & 1 deletion docs/development/sync.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
By default, `pybotx` provides asynchronous methods through the `Bot` class, since this is more efficient.
But as an alternative, you can import the `Bot` from the `botx.sync` module and use all the methods as normal blockable
functions. Handlers will then be dispatched using the `concurrent.futures.threads.ThreadPoolExecutor` object.
functions. Handlers will then be dispatched using the `concurrent.futures.threads.ThreadPoolExecutor` object.


If you write synchronous handlers using the asynchronous `Bot` from the `botx` package, then you can call the client
function to send data in the same way as if it were synchronous::

```python3
from botx import Bot, Message

bot = Bot()

@bot.handler
async def async_handler(message: Message, bot: Bot):
await bot.answer_message(message.body, message)

@bot.handler
def sync_handler(message: Message, bot: Bot):
bot.answer_message(message.body, message)

```
Loading

0 comments on commit ae976b7

Please sign in to comment.