Skip to content

Commit

Permalink
Merge pull request #9 from erayerdin/development
Browse files Browse the repository at this point in the history
v0.1.0b2
  • Loading branch information
erayerdin committed May 19, 2019
2 parents e088f86 + 50b27e7 commit 6f6357c
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 135 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.1.0b2] - 2019-05-19
### Changed
- Refactored `tglogger.request` module to use simple function called `send_log`

## [v0.1.0b1] - 2019-05-14
### Added
- Explicit Django support
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Assuming you have a logger instance:
logger = logging.getLogger(__name__)
```

You need to have an instance of `TelegramHandler` handler
You need to have an instance of `TelegramHandler` and
`TelegramFormatter`.

```python
Expand All @@ -66,7 +66,7 @@ to the chat you have defined with `receiver` by the bot that you
have defined by `bot_token`.

```python
logger.error("foo") # you will receiver message by your bot
logger.error("foo") # you will receive a message by your bot
```

## Documentation
Expand Down
41 changes: 41 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Limitations

`tglogger` is not meant to be a replacement for cloud logging services such as Sentry
or Rollbar and there are clear limitations on what you can do with `tglogger`. In this
section, it is aimed to be mention these limitations.

## Cloud Logging Services

Cloud logging services provide you

- the live log captures
- the history of logging
- stats and charts of log records
- email notifications

and many more. That is why the services you get from these are going to be far superior
than using `tglogger` alone. `tglogger` is meant to be a helper for collaborative and
small-scoped projects and it currently helps you to only;

- capture the live logging records and
- the history of logging

with some limitations from both the side of Telegram and this library's way of doing things.

Also consider that cloud logging services already provide free services with their limitations.

## Telegram Bot Request Throttle

[According to Telegram's FAQ on bots](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this),
Telegram limits the requests made to the server with the amounts below:

- **30 messages** to *same individual* per **one second or so**
- **1 message** to *different individuals* per **one second or so**
- **20 messages** to *same group* per **one minute**

The table below can give you idea in what amount you can send logs *theoretically*:

| | per minute | per hour | per day | per week | per month (30 days) |
|---|---|---|---|---|---|
| **individual** | 1.800 | 108.000 | 2.592.000 | 18.144.000 | 544.320.000 |
| **group** | 20 | 1.200 | 28.800 | 201.600 | 6.048.000 |
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ nav:
- Home: index.md
- Getting Started: getting-started.md
- Django Integration: django.md
- Limitations: limitations.md
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
include_package_data=True,
keywords="telegram messaging communication logging",
classifiers=[
"Development Status :: 3 - Alpha",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: Apache Software License",
Expand Down
36 changes: 5 additions & 31 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,11 @@


@pytest.fixture
def mock_adapter() -> requests_mock.Adapter:
return requests_mock.Adapter()


@pytest.fixture
def bot_session(mock_adapter) -> tglogger.request.BotSession:
session = tglogger.request.BotSession("0")
session.mount("mock", mock_adapter)
session._is_mocked = True
return session


@pytest.fixture
def bot_request_factory(bot_session) -> callable:
def factory(method_name: str) -> tglogger.request.BotRequest:
return tglogger.request.BotRequest(bot_session, method_name)

return factory


@pytest.fixture
def bot_send_message_request(
bot_session
) -> tglogger.request.SendMessageRequest:
return tglogger.request.SendMessageRequest(bot_session, 1, "foo")


@pytest.fixture
def request_body_factory() -> callable:
def factory(request: requests_mock.request.requests.Request) -> dict:
return json.loads(request.body.decode("utf-8"))
def read_test_resource(request):
def factory(file_name: str, mode="r"):
file = open("tests/resources/{}".format(file_name), mode)
request.addfinalizer(lambda: file.close())
return file

return factory

Expand Down
13 changes: 13 additions & 0 deletions tests/resources/message.response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"ok": true,
"result": {
"message_id": 1,
"from": {
"id": 1,
"is_bot": true,
"username": "foobot"
},
"date": 1558047713,
"chat": 1
}
}
1 change: 0 additions & 1 deletion tests/test_django.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def factory(USE_TZ=True, TIME_ZONE="Europe/Istanbul"):

log_record = log_record_factory()
message = telegram_formatter.format(log_record)
print(message)
return message

return factory
Expand Down
7 changes: 6 additions & 1 deletion tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ class TestTelegramHandler:
)
def test_emit_return_type(self, telegram_handler, log_record_factory):
log_record = log_record_factory()
assert isinstance(telegram_handler.emit(log_record), requests.Response)

responses = telegram_handler.emit(log_record)
assert isinstance(responses, dict)

for value in responses.values():
assert isinstance(value, requests.Response) or value is None
107 changes: 74 additions & 33 deletions tests/test_request.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,86 @@
import re
from urllib.parse import parse_qs

import pytest

import tglogger.request


class TestBotSession:
def test_has_token(self, bot_session):
assert hasattr(bot_session, "token")
@pytest.fixture
def send_log_responses(
telegram_handler, log_record_factory, requests_mock, read_test_resource
):
send_message_rule = re.compile(
"https:\/\/api.telegram.org\/bot.+\/sendMessage"
)
send_document_rule = re.compile(
"https:\/\/api.telegram.org\/bot.+\/sendDocument"
)

requests_mock.register_uri(
"POST",
send_message_rule,
text=read_test_resource("message.response.json").read(),
)
requests_mock.register_uri(
"POST",
send_document_rule,
text=read_test_resource("message.response.json").read(),
)

log_record = log_record_factory()
return tglogger.request.send_log(telegram_handler, log_record, 1)


@pytest.fixture
def generic_info_response(send_log_responses):
return send_log_responses["generic_info_response"]


@pytest.fixture
def generic_info_request(generic_info_response):
return generic_info_response.request


@pytest.fixture
def generic_info_request_body(generic_info_request):
return parse_qs(generic_info_request.body)


class TestSendLogReturn:
def test_isinstance(self, send_log_responses):
assert isinstance(send_log_responses, dict)

def test_length(self, send_log_responses):
assert len(send_log_responses) == 3

def test_has_base_url(self, bot_session):
assert hasattr(bot_session, "base_url")
def test_key_isinstance(self, send_log_responses):
for key in send_log_responses:
assert isinstance(key, str)

def test_value_isinstance(self, send_log_responses):
for _, value in send_log_responses.items():
assert (
isinstance(value, tglogger.request.requests.Response)
or value is None
)

class TestBotRequest:
def setup_method(self):
self.session = tglogger.request.BotSession("0")
self.request = tglogger.request.BotRequest(self.session, "getMe")
def test_generic_info_response_key_name(self, send_log_responses):
assert "generic_info_response" in send_log_responses

def test_url(self, bot_session, bot_request_factory):
bot_request = bot_request_factory("getMe")
url = "{base_url}{method_name}".format(
base_url=bot_session.base_url, method_name="getMe"
)
assert bot_request.url == url
def test_stack_trace_response_key_name(self, send_log_responses):
assert "stack_trace_response" in send_log_responses

def test_django_settings_response_key_name(self, send_log_responses):
assert "django_settings_response" in send_log_responses

class TestSendMessageRequest:
def test_url(self, bot_send_message_request):
assert bot_send_message_request.url[-11:] == "sendMessage"

def test_request_body_chat_id(
self, bot_send_message_request, request_body_factory
):
request_body = request_body_factory(bot_send_message_request)
assert request_body.get("chat_id") == 1
class TestSendLogGenericInfoRequest:
def test_body_chat_id(self, generic_info_request_body):
assert generic_info_request_body["chat_id"][0] == "1"

def test_request_body_text(
self, bot_send_message_request, request_body_factory
):
request_body = request_body_factory(bot_send_message_request)
assert request_body.get("text") == "foo"
def test_body_parse_mode(self, generic_info_request_body):
assert generic_info_request_body["parse_mode"][0] == "markdown"

def test_request_body_parse_mode(
self, bot_send_message_request, request_body_factory
):
request_body = request_body_factory(bot_send_message_request)
assert request_body.get("parse_mode") == "Markdown"
def test_body_text(self, generic_info_request_body):
assert "text" in generic_info_request_body
2 changes: 1 addition & 1 deletion tglogger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "0.1.0b1"
__version__ = "0.1.0b2"
__author__ = "Eray Erdin"
10 changes: 3 additions & 7 deletions tglogger/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ def __init__(
bot_token: str = os.environ["TELEGRAM_BOT_TOKEN"],
receiver: str = os.environ["TELEGRAM_RECEIVER"],
):
self._bot_token = bot_token
self._receiver = receiver
self.bot_token = bot_token
self.receiver = receiver
super().__init__(level)

def emit(self, record):
session = request.BotSession(token=self._bot_token)
req = request.SendMessageRequest(
session, self._receiver, self.format(record)
)
return session.send(req)
return request.send_log(self, record, self.receiver)
79 changes: 21 additions & 58 deletions tglogger/request.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,28 @@
import logging
import typing

import requests

_BASE_URL = "https://api.telegram.org/bot{token}/{method}"

class BotSession(requests.Session):
def __init__(self, token: str):
super().__init__()
self.token = str(token)
self._is_mocked = False

@property
def base_url(self):
if self._is_mocked:
scheme = "mock"
else:
scheme = "https"

return "{scheme}://api.telegram.org/bot{token}/".format(
scheme=scheme, token=self.token
)


class BotRequest(requests.PreparedRequest):
def __init__(self, session: BotSession, method_name: str):
super().__init__()
self.__session = session
self.__method_name = method_name
self.prepare()

def prepare_url(self, url, params):
super(BotRequest, self).prepare_url(
"{base_url}{method_name}".format(
base_url=self.__session.base_url,
method_name=self.__method_name,
),
params,
)


class SendMessageRequest(BotRequest):
def __init__(
self,
session: BotSession,
chat_id: typing.Union[str, int],
text: str,
parse_mode: str = "Markdown",
disable_web_page_preview: bool = False,
disable_notification: bool = False,
):
try:
chat_id = int(chat_id)
except ValueError: # pragma: no cover
pass # pragma: no cover

super().__init__(session, "sendMessage")
self.prepare_method("post")
payload = {
def send_log(
handler: logging.Handler, record: logging.LogRecord, chat_id: int
) -> typing.Dict[str, requests.Response]:
"""
Sends log to Telegram chat.
"""
generic_info_response = requests.post(
url=_BASE_URL.format(token=handler.bot_token, method="sendMessage"),
data={
"chat_id": chat_id,
"text": str(text),
"parse_mode": str(parse_mode),
"disable_web_page_preview": bool(disable_web_page_preview),
"disable_notification": bool(disable_notification),
}
self.prepare_body(data=None, files=None, json=payload)
"text": handler.format(record),
"parse_mode": "markdown",
},
)

return {
"generic_info_response": generic_info_response,
"stack_trace_response": None,
"django_settings_response": None,
}

0 comments on commit 6f6357c

Please sign in to comment.