Merge pull request #1 from DarkCat09/main

Refactoring
This commit is contained in:
Данил 2023-05-30 14:03:49 +03:00 committed by GitHub
commit de3b3a393b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 383 additions and 70 deletions

7
.gitignore vendored
View file

@ -1 +1,8 @@
.idea .idea
.vscode
__pycache__
.mypy_cache
venv
.env

View file

@ -1,4 +1,4 @@
Copyright (c) 2023 Redume Copyright (c) 2023 Redume, DarkCat09 (Chechkenev Andrey)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# Shirino
## Что это?
Бот для телеграма. Выводит курс валют.
https://t.me/Shirino_bot
## Хочу запустить
Получите токен бота в телеграме и токен CoinAPI.
Вставьте в файл `.env` в формате:
```
COINAPI_KEY=тут для койнапи
TELEGRAM_TOKEN=тут для телеграма
```
В энв файл ещё можно такие переменные добавить:
```
DEBUG=false или true, включает отладочные логи
TIMEOUT=таймаут для библиотеки requests, в секундах (2 по дефолту)
```
## Хочу сделать пулл-реквест
Ставьте pylint и mypy для статической проверки кода.
Конфиги уже есть в репозитории.
После проверок можете открывать PR.

217
main.py
View file

@ -1,103 +1,184 @@
import requests #!/usr/bin/env python3
import json import json
import hashlib import hashlib
from aiogram import Bot, types import logging
from aiogram.dispatcher import Dispatcher from typing import Any, Dict, List, Optional
from aiogram.utils import executor
import requests
from pydantic import BaseSettings
from aiogram import Bot # type: ignore
from aiogram.dispatcher import Dispatcher # type: ignore
from aiogram.utils import executor # type: ignore
from aiogram.types import InlineQuery # type: ignore
from aiogram.types import InlineQueryResultArticle
from aiogram.types import InputTextMessageContent
bot = Bot(token="5193950006:AAGU8elNfNB9FocVSIb4DnqoEvQk70Mqh5E") # Constants
DDG_URL = 'https://duckduckgo.com/js/spice/currency'
COINAPI_URL = 'https://rest.coinapi.io/v1/exchangerate'
# ---
# Config from .env
class Settings(BaseSettings):
debug: bool
timeout: int = 2
coinapi_key: str
telegram_token: str
class Config:
env_file = '.env'
env_file_encoding = 'utf-8'
settings = Settings() # type: ignore
# ---
# Logging
log = logging.getLogger('shirino')
handler = logging.StreamHandler()
fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
handler.setFormatter(fmt)
log.addHandler(handler)
if settings.debug:
handler.setLevel(logging.DEBUG)
log.setLevel(logging.DEBUG)
# ---
bot = Bot(token=settings.telegram_token)
dp = Dispatcher(bot) dp = Dispatcher(bot)
class TypeDict: class CurrencyConverter:
def __init__(self):
self.amount = None
self.from_amount = None
self.from_currency = None
self.to_currency = None
@staticmethod def __init__(self) -> None:
def get_currency(self, amount, from_currency, to_currency):
if amount is None:
amount = "1"
try: self.amount = 1.0
page = requests.get(f"https://duckduckgo.com/js/spice/currency/{amount}/{from_currency}/{to_currency}") self.conv_amount = 0.0
page = page.text.replace('ddg_spice_currency(', "").replace(');', "") self.from_currency = ''
page = json.loads(page) self.conv_currency = ''
if page["headers"]["description"].find("ERROR") != -1: def convert(self) -> None:
print(from_currency, to_currency) """Currency conversion"""
crypto = requests.get(f"https://rest.coinapi.io/v1/exchangerate/{from_currency.upper()}/{to_currency.upper()}", headers={
"X-CoinAPI-Key": "8A465920-C233-4EE2-860B-A0AF9EC21FFF"
}).json()
print(crypto) if not self.ddgapi():
self.coinapi()
self.from_amount = crypto.get("rate") def ddgapi(self) -> bool:
return crypto.get("rate") """Get data from DuckDuckGo's currency API
except KeyError: Returns:
print("blyat slomal") `False` if the currency does not exist,
return None `True` on successful conversion
"""
return page.get("conversion") # API request
resp = requests.get(
(
f'{DDG_URL}/{self.amount}/{self.from_currency}'
f'/{self.conv_currency}'
),
timeout=settings.timeout,
)
@staticmethod log.debug(resp.text)
def is_num(value):
return value.isdecimal() or value.replace('.', '', 1).isdecimal()
# Parsing JSON data
data: Dict[str, Any] = json.loads(
resp.text
.replace('ddg_spice_currency(', '')
.replace(');', '')
)
type_dict = TypeDict() log.debug(data)
# If the currency does not exist
descr = data.get('headers', {}).get('description', '')
if descr.find('ERROR') != -1:
return False
# Otherwise
conv: Dict[str, str] = data.get('conversion', {})
self.conv_amount = float(conv.get('converted-amount', 0))
log.debug(conv)
return True
def coinapi(self) -> None:
"""Get data from CoinAPI (for cryptocurrencies)"""
resp = requests.get(
(
f'{COINAPI_URL}/{self.from_currency}'
f'/{self.conv_currency}'
),
headers={
'X-CoinAPI-Key': settings.coinapi_key,
},
timeout=settings.timeout,
)
data: Dict[str, Any] = resp.json()
self.conv_amount = float(data.get('rate', 0))
@dp.inline_handler() @dp.inline_handler()
async def currency(query: types.InlineQuery): async def currency(inline_query: InlineQuery) -> None:
text = query.query.split(" ")
result_id: str = hashlib.md5(query.query.encode()).hexdigest()
if text == ['']: query = inline_query.query
return article: List[Optional[InlineQueryResultArticle]] = [None]
for i in range(len(text)): text = query.split()
if type_dict.is_num(text[i]): len_ = len(text)
continue
if text[i].find(",") != -1: result_id = hashlib.md5(query.encode()).hexdigest()
text[i] = text[i].replace(",", ".") conv = CurrencyConverter()
try: try:
if type_dict.is_num(text[0]): if len_ == 3:
res, crypto_rate = type_dict.get_currency(text[0], text[1], text[2]) conv.amount = float(text[0])
conv.from_currency = text[1].upper()
conv.conv_currency = text[2].upper()
conv.convert()
elif len_ == 2:
conv.from_currency = text[0].upper()
conv.conv_currency = text[1].upper()
conv.convert()
else: else:
res, crypto_rate = type_dict.get_currency(None, text[0], text[1]) raise ValueError('Надо 2 или 3 аргумента')
except Exception:
return
if res is None: result = (
return f'{conv.amount} {conv.from_currency} = '
f'{conv.conv_amount} {conv.conv_currency}'
)
print(res) except Exception as ex:
result = f'{type(ex).__name__}: {ex}'
from_amount = res.get('from_amount', res['from-amount']) article[0] = InlineQueryResultArticle(
from_currency_symbol = res.get('from_currency_symbol', res['from-currency-symbol'])
converted_amount = res.get('converted_amount', res['converted-amount'])
to_currency_symbol = res.get('to_currency_symbol', res['to-currency-symbol'])
result = f"{from_amount} {from_currency_symbol} = {converted_amount} {to_currency_symbol}"
if crypto_rate:
result += f" | Crypto Rate: {crypto_rate}"
article = [types.InlineQueryResultArticle(
id=result_id, id=result_id,
title=result, title=result,
input_message_content=types.InputTextMessageContent( input_message_content=InputTextMessageContent(
message_text=result message_text=result,
))] ),
)
await inline_query.answer(
article,
cache_time=1,
is_personal=True,
)
await query.answer(article, cache_time=1, is_personal=True)
executor.start_polling(dp, skip_updates=True) executor.start_polling(dp, skip_updates=True)

5
mypy.ini Normal file
View file

@ -0,0 +1,5 @@
[mypy]
check_untyped_defs = True
warn_return_any = True
warn_unreachable = True
show_error_codes = True

193
pylintrc Normal file
View file

@ -0,0 +1,193 @@
[MAIN]
analyse-fallback-blocks=no
clear-cache-post-run=no
extension-pkg-allow-list=
extension-pkg-whitelist=
fail-on=
fail-under=10
ignore=CVS
ignore-paths=
ignore-patterns=^\.#
ignored-modules=
jobs=4
limit-inference-results=100
load-plugins=
persistent=yes
py-version=3.10
recursive=no
suggestion-mode=yes
unsafe-load-any-extension=no
[BASIC]
argument-naming-style=snake_case
attr-naming-style=snake_case
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
bad-names-rgxs=
class-attribute-naming-style=any
class-const-naming-style=UPPER_CASE
class-naming-style=PascalCase
const-naming-style=UPPER_CASE
docstring-min-length=-1
function-naming-style=snake_case
good-names=i,
j,
k,
ex,
Run,
_
good-names-rgxs=
include-naming-hint=no
inlinevar-naming-style=any
method-naming-style=snake_case
module-naming-style=snake_case
name-group=
no-docstring-rgx=^_
property-classes=abc.abstractproperty
variable-naming-style=snake_case
[CLASSES]
check-protected-access-in-special-methods=no
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
valid-classmethod-first-arg=cls
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
exclude-too-few-public-methods=
ignored-parents=
max-args=5
max-attributes=7
max-bool-expr=5
max-branches=12
max-locals=15
max-parents=7
max-public-methods=20
max-returns=6
max-statements=50
min-public-methods=0
[EXCEPTIONS]
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
expected-line-ending-format=
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
indent-after-paren=4
indent-string=' '
max-line-length=100
max-module-lines=1000
single-line-class-stmt=no
single-line-if-stmt=no
[IMPORTS]
allow-any-import-level=
allow-reexport-from-package=no
allow-wildcard-with-all=no
deprecated-modules=
ext-import-graph=
import-graph=
int-import-graph=
known-standard-library=
known-third-party=enchant
preferred-modules=
[LOGGING]
logging-format-style=old
logging-modules=logging
[MESSAGES CONTROL]
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
missing-module-docstring,
missing-class-docstring,
missing-function-docstring,
broad-exception-caught
enable=c-extension-no-member
[METHOD_ARGS]
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
notes=FIXME,
XXX,
TODO
notes-rgx=
[REFACTORING]
max-nested-blocks=5
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
msg-template=
reports=no
score=yes
[SIMILARITIES]
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes
ignore-signatures=yes
min-similarity-lines=4
[SPELLING]
max-spelling-suggestions=4
spelling-dict=
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
spelling-ignore-words=
spelling-private-dict-file=
spelling-store-unknown-words=no
[STRING]
check-quote-consistency=no
check-str-concat-over-line-jumps=no
[TYPECHECK]
contextmanager-decorators=contextlib.contextmanager
generated-members=
ignore-none=yes
ignore-on-opaque-inference=yes
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
missing-member-hint=yes
missing-member-hint-distance=1
missing-member-max-choices=1
mixin-class-rgx=.*[Mm]ixin
signature-mutators=
[VARIABLES]
additional-builtins=
allow-global-unused-variables=yes
allowed-redefined-builtins=
callbacks=cb_,
_cb
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
ignored-argument-names=_.*|^ignored_|^unused_
init-import=no
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
requests==2.31.0
aiogram==2.25.1
pydantic[dotenv]==1.10.8