1
0
mirror of https://github.com/eternnoir/pyTelegramBotAPI.git synced 2023-08-10 21:12:57 +03:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Badiboy
91badb53e5
Merge pull request #1473 from coder2020official/master
Docstrings of sync telebot updated to fit documentation. Basic middleware classes added.
2022-03-07 20:00:55 +03:00
coder2020official
477d02468d Fixed middlewares 2022-03-07 21:40:39 +05:00
coder2020official
9f3a270fae Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-03-07 21:13:49 +05:00
coder2020official
244b058648 Fix 2022-03-07 21:13:30 +05:00
_run
886c9b9bc0
Update README.md 2022-03-07 20:38:22 +05:00
_run
41025ba97b
Update setup_python.yml 2022-03-07 20:26:11 +05:00
_run
4875bb6188
Update requirements.txt 2022-03-07 19:04:12 +05:00
_run
5f91c3d4e6
Added python 3.10 to tests 2022-03-07 18:56:16 +05:00
_run
7b62915a5b
Create PULL_REQUEST_TEMPLATE.md 2022-03-07 18:54:59 +05:00
_run
05c3cb2c1d
Update doc_req.txt 2022-03-07 18:08:41 +05:00
_run
60a96d1400
Update README.md 2022-03-07 17:42:47 +05:00
_run
dcd0df93da
Update README.md 2022-03-07 17:39:42 +05:00
_run
f15101fc6f
Update doc_req.txt 2022-03-07 17:32:42 +05:00
coder2020official
5f03253398 Fix comments 2022-03-07 17:31:02 +05:00
_run
8fab55e937
Create antiflood_middleware.py 2022-03-07 17:30:10 +05:00
_run
60a23665cb
Update flooding_middleware.py 2022-03-07 17:23:55 +05:00
_run
b292b275cb
Create basic_example.py 2022-03-07 17:21:59 +05:00
_run
403028bf35
Update README.md 2022-03-07 17:14:51 +05:00
_run
3dda5cff06
Create README.md 2022-03-07 17:14:25 +05:00
coder2020official
dd589e2490 Updated documentation to another theme. 2022-03-07 16:10:44 +05:00
coder2020official
c8fb83c97c Fix documentation 2022-03-07 14:24:28 +05:00
coder2020official
854f6a9506 Merge branch 'master' of https://github.com/coder2020official/pyTelegramBotAPI 2022-03-07 13:30:41 +05:00
coder2020official
be0557c2b5 Multiple middlewares allowed for async 2022-03-07 13:30:39 +05:00
_run
fc72576aaa
Update README.md 2022-03-07 00:39:25 +05:00
coder2020official
f69a2ba044 Update docstrings for asynctelebot. 2022-03-07 00:18:11 +05:00
coder2020official
c45e06c694 Updated description for TeleBot class 2022-03-06 23:23:33 +05:00
coder2020official
78bdf1ca4e Update docstrings 2022-03-06 23:14:07 +05:00
coder2020official
3c7d3c0196 Fix tests 2022-03-06 19:52:42 +05:00
coder2020official
441a5793cc Update docstrings to correct documentation. 2022-03-06 19:41:54 +05:00
coder2020official
388477686b Added middlewares.
Bumped middlewares
2022-03-06 18:39:41 +05:00
23 changed files with 1262 additions and 479 deletions

14
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,14 @@
## Description
Include changes, new features and etc:
## Describe your tests
How did you test your change?
Python version:
OS:
## Checklist:
- [ ] I added/edited example on new feature/change (if exists)
- [ ] My changes won't break backend compatibility

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ '3.6','3.7','3.8','3.9', 'pypy-3.7', 'pypy-3.8' ] #'pypy-3.9' NOT SUPPORTED NOW python-version: [ '3.6','3.7','3.8','3.9', '3.10', 'pypy-3.7', 'pypy-3.8', 'pypy-3.9']
name: ${{ matrix.python-version }} and tests name: ${{ matrix.python-version }} and tests
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

2
.gitignore vendored
View File

@ -67,5 +67,3 @@ testMain.py
# documentation # documentation
_build/ _build/
_static/
_templates/

View File

@ -1,15 +1,17 @@
[![PyPi Package Version](https://img.shields.io/pypi/v/pyTelegramBotAPI.svg)](https://pypi.python.org/pypi/pyTelegramBotAPI) [![PyPi Package Version](https://img.shields.io/pypi/v/pyTelegramBotAPI.svg)](https://pypi.python.org/pypi/pyTelegramBotAPI)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/pyTelegramBotAPI.svg)](https://pypi.python.org/pypi/pyTelegramBotAPI) [![Supported Python versions](https://img.shields.io/pypi/pyversions/pyTelegramBotAPI.svg)](https://pypi.python.org/pypi/pyTelegramBotAPI)
[![Documentation Status](https://readthedocs.org/projects/pytba/badge/?version=latest)](https://pytba.readthedocs.io/en/latest/?badge=latest)
[![Build Status](https://travis-ci.org/eternnoir/pyTelegramBotAPI.svg?branch=master)](https://travis-ci.org/eternnoir/pyTelegramBotAPI) [![Build Status](https://travis-ci.org/eternnoir/pyTelegramBotAPI.svg?branch=master)](https://travis-ci.org/eternnoir/pyTelegramBotAPI)
[![PyPi downloads](https://img.shields.io/pypi/dm/pyTelegramBotAPI.svg)](https://pypi.org/project/pyTelegramBotAPI/) [![PyPi downloads](https://img.shields.io/pypi/dm/pyTelegramBotAPI.svg)](https://pypi.org/project/pyTelegramBotAPI/)
[![PyPi status](https://img.shields.io/pypi/status/pytelegrambotapi.svg?style=flat-square)](https://pypi.python.org/pypi/pytelegrambotapi)
# <p align="center">pyTelegramBotAPI # <p align="center">pyTelegramBotAPI
<p align="center">A simple, but extensible Python implementation for the <a href="https://core.telegram.org/bots/api">Telegram Bot API</a>.</p> <p align="center">A simple, but extensible Python implementation for the <a href="https://core.telegram.org/bots/api">Telegram Bot API</a>.</p>
<p align="center">Supports both sync and async ways.</p> <p align="center">Both synchronous and asynchronous.</p>
## <p align="center">Supporting Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>! ## <p align="center">Supported Bot API version: <a href="https://core.telegram.org/bots/api#january-31-2022">5.7</a>!
<h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2> <h2><a href='https://pytba.readthedocs.io/en/latest/index.html'>Official documentation</a></h2>
@ -354,7 +356,9 @@ def start(message):
assert message.another_text == message.text + ':changed' assert message.another_text == message.text + ':changed'
``` ```
There are other examples using middleware handler in the [examples/middleware](examples/middleware) directory. There are other examples using middleware handler in the [examples/middleware](examples/middleware) directory.
#### Class-based middlewares
There are class-based middlewares. Check out in [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/middleware/class_based)
#### Custom filters #### Custom filters
Also, you can use built-in custom filters. Or, you can create your own filter. Also, you can use built-in custom filters. Or, you can create your own filter.
@ -757,7 +761,7 @@ As you can see here, keywords are await and async.
Asynchronous tasks depend on processor performance. Many asynchronous tasks can run parallelly, while thread tasks will block each other. Asynchronous tasks depend on processor performance. Many asynchronous tasks can run parallelly, while thread tasks will block each other.
### Differences in AsyncTeleBot ### Differences in AsyncTeleBot
AsyncTeleBot has different middlewares. See example on [middlewares](https://github.com/coder2020official/pyTelegramBotAPI/tree/master/examples/asynchronous_telebot/middleware) AsyncTeleBot is asynchronous. It uses aiohttp instead of requests module.
### Examples ### Examples
See more examples in our [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/asynchronous_telebot) folder See more examples in our [examples](https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples/asynchronous_telebot) folder

View File

@ -1 +1,3 @@
pytelegrambotapi pytelegrambotapi
furo
sphinx_copybutton

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -28,14 +28,6 @@ Asynchronous storage for states
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
asyncio_helper file
-------------------
.. automodule:: telebot.asyncio_helper
:members:
:undoc-members:
:show-inheritance:
Asyncio handler backends Asyncio handler backends
------------------------ ------------------------

View File

@ -22,7 +22,7 @@ copyright = '2022, coder2020official'
author = 'coder2020official' author = 'coder2020official'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '1.0' release = '4.4.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
@ -31,11 +31,11 @@ release = '1.0'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx_rtd_theme',
'sphinx.ext.autosectionlabel', 'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
"sphinx.ext.autosummary", "sphinx.ext.autosummary",
"sphinx.ext.napoleon", "sphinx.ext.napoleon",
"sphinx_copybutton",
] ]
@ -53,14 +53,18 @@ exclude_patterns = []
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
html_theme = 'sphinx_rtd_theme' html_theme = 'furo'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ['_static']
html_logo = 'logo.png' #html_logo = 'logo.png'
html_theme_options = { html_theme_options = {
'logo_only': True, "light_css_variables": {
'display_version': False, "color-brand-primary": "#7C4DFF",
"color-brand-content": "#7C4DFF",
},
"light_logo": "logo.png",
"dark_logo": "logo2.png",
} }

View File

@ -25,7 +25,7 @@ Pypi: `Pypi <https://pypi.org/project/pyTelegramBotAPI/>`__
Source: `Github repository <https://github.com/eternnoir/pyTelegramBotAPI>`__ Source: `Github repository <https://github.com/eternnoir/pyTelegramBotAPI>`__
Some features: Some features:
------------- --------------
Easy to learn and use. Easy to learn and use.
Easy to understand. Easy to understand.

View File

@ -3,8 +3,6 @@ Installation Guide
================== ==================
:toctree
Using PIP Using PIP
---------- ----------
.. code-block:: bash .. code-block:: bash

View File

@ -33,11 +33,3 @@ handler_backends file
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
apihelper
------------------------
.. automodule:: telebot.apihelper
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,8 +1,7 @@
# Just a little example of middleware handlers # Just a little example of middleware handlers
from telebot.asyncio_handler_backends import BaseMiddleware from telebot.asyncio_handler_backends import BaseMiddleware, CancelUpdate
from telebot.async_telebot import AsyncTeleBot from telebot.async_telebot import AsyncTeleBot
from telebot.async_telebot import CancelUpdate
bot = AsyncTeleBot('TOKEN') bot = AsyncTeleBot('TOKEN')
@ -36,4 +35,4 @@ async def start(message):
await bot.send_message(message.chat.id, 'Hello!') await bot.send_message(message.chat.id, 'Hello!')
import asyncio import asyncio
asyncio.run(bot.polling()) asyncio.run(bot.polling())

View File

@ -0,0 +1,35 @@
# Middlewares
## Type of middlewares in pyTelegramBotAPI
Currently, synchronous version of pyTelegramBotAPI has two types of middlewares:
- Class-based middlewares
- Function-based middlewares
## Purpose of middlewares
Middlewares are designed to get update before handler's execution.
## Class-based middlewares
This type of middleware has more functionality compared to function-based one.
Class based middleware should be instance of `telebot.handler_backends.BaseMiddleware.`
Each middleware should have 2 main functions:
`pre_process` -> is a method of class which receives update, and data.
Data - is a dictionary, which could be passed right to handler, and `post_process` function.
`post_process` -> is a function of class which receives update, data, and exception, that happened in handler. If handler was executed correctly - then exception will equal to None.
## Function-based middlewares
To use function-based middleware, you should set `apihelper.ENABLE_MIDDLEWARE = True`.
This type of middleware is created by using a decorator for middleware.
With this type middleware, you can retrieve update immediately after update came. You should set update_types as well.
## Why class-based middlewares are better?
- You can pass data between post, pre_process functions, and handler.
- If there is an exception in handler, you will get exception parameter with exception class in post_process.
- Has post_process -> method which works after the handler's execution.
## Take a look at examples for more.

View File

@ -0,0 +1,39 @@
# Just a little example of middleware handlers
from telebot.handler_backends import BaseMiddleware
from telebot import TeleBot
from telebot.handler_backends import CancelUpdate
bot = TeleBot('TOKEN',
use_class_middlewares=True) # if you don't set it to true, middlewares won't work
class SimpleMiddleware(BaseMiddleware):
def __init__(self, limit) -> None:
self.last_time = {}
self.limit = limit
self.update_types = ['message']
# Always specify update types, otherwise middlewares won't work
def pre_process(self, message, data):
if not message.from_user.id in self.last_time:
# User is not in a dict, so lets add and cancel this function
self.last_time[message.from_user.id] = message.date
return
if message.date - self.last_time[message.from_user.id] < self.limit:
# User is flooding
bot.send_message(message.chat.id, 'You are making request too often')
return CancelUpdate()
self.last_time[message.from_user.id] = message.date
def post_process(self, message, data, exception):
pass
bot.setup_middleware(SimpleMiddleware(2))
@bot.message_handler(commands=['start'])
def start(message): # you don't have to put data in handler.
bot.send_message(message.chat.id, 'Hello!')
bot.infinity_polling()

View File

@ -0,0 +1,32 @@
from telebot import TeleBot
from telebot.handler_backends import BaseMiddleware
bot = TeleBot('TOKEN', use_class_middlewares=True) # set use_class_middlewares to True!
# otherwise, class-based middlewares won't execute.
# You can use this classes for cancelling update or skipping handler:
# from telebot.handler_backends import CancelUpdate, SkipHandler
class Middleware(BaseMiddleware):
def __init__(self):
self.update_types = ['message']
def pre_process(self, message, data):
data['foo'] = 'Hello' # just for example
# we edited the data. now, this data is passed to handler.
# return SkipHandler() -> this will skip handler
# return CancelUpdate() -> this will cancel update
def post_process(self, message, data, exception=None):
print(data['foo'])
if exception: # check for exception
print(exception)
@bot.message_handler(commands=['start'])
def start(message, data: dict): # you don't have to put data parameter in handler if you don't need it.
bot.send_message(message.chat.id, data['foo'])
data['foo'] = 'Processed' # we changed value of data.. this data is now passed to post_process.
# Setup middleware
bot.setup_middleware(Middleware())
bot.infinity_polling()

View File

@ -1,4 +1,4 @@
pytest==3.0.2 pytest
requests==2.20.0 requests==2.20.0
wheel==0.24.0 wheel==0.24.0
aiohttp>=3.8.0,<3.9.0 aiohttp>=3.8.0,<3.9.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,33 @@ class State:
class StatesGroup: class StatesGroup:
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
# print all variables of a subclass
for name, value in cls.__dict__.items(): for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State): if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable # change value of that variable
value.name = ':'.join((cls.__name__, name)) value.name = ':'.join((cls.__name__, name))
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass

View File

@ -159,10 +159,48 @@ class State:
class StatesGroup: class StatesGroup:
def __init_subclass__(cls) -> None: def __init_subclass__(cls) -> None:
# print all variables of a subclass
for name, value in cls.__dict__.items(): for name, value in cls.__dict__.items():
if not name.startswith('__') and not callable(value) and isinstance(value, State): if not name.startswith('__') and not callable(value) and isinstance(value, State):
# change value of that variable # change value of that variable
value.name = ':'.join((cls.__name__, name)) value.name = ':'.join((cls.__name__, name))
class BaseMiddleware:
"""
Base class for middleware.
Your middlewares should be inherited from this class.
"""
def __init__(self):
pass
def pre_process(self, message, data):
raise NotImplementedError
def post_process(self, message, data, exception):
raise NotImplementedError
class SkipHandler:
"""
Class for skipping handlers.
Just return instance of this class
in middleware to skip handler.
Update will go to post_process,
but will skip execution of handler.
"""
def __init__(self) -> None:
pass
class CancelUpdate:
"""
Class for canceling updates.
Just return instance of this class
in middleware to skip update.
Update will skip handler and execution
of post_process in middlewares.
"""
def __init__(self) -> None:
pass

View File

@ -1041,9 +1041,9 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
def __init__(self, keyboard=None, row_width=3): def __init__(self, keyboard=None, row_width=3):
""" """
This object represents an inline keyboard that appears This object represents an inline keyboard that appears
right next to the message it belongs to. right next to the message it belongs to.
:return: :return: None
""" """
if row_width > self.max_row_keys: if row_width > self.max_row_keys:
# Todo: Will be replaced with Exception in future releases # Todo: Will be replaced with Exception in future releases
@ -1058,10 +1058,10 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
This method adds buttons to the keyboard without exceeding row_width. This method adds buttons to the keyboard without exceeding row_width.
E.g. InlineKeyboardMarkup.add("A", "B", "C") yields the json result: E.g. InlineKeyboardMarkup.add("A", "B", "C") yields the json result:
{keyboard: [["A"], ["B"], ["C"]]} {keyboard: [["A"], ["B"], ["C"]]}
when row_width is set to 1. when row_width is set to 1.
When row_width is set to 2, the result: When row_width is set to 2, the result:
{keyboard: [["A", "B"], ["C"]]} {keyboard: [["A", "B"], ["C"]]}
See https://core.telegram.org/bots/api#inlinekeyboardmarkup See https://core.telegram.org/bots/api#inlinekeyboardmarkup
:param args: Array of InlineKeyboardButton to append to the keyboard :param args: Array of InlineKeyboardButton to append to the keyboard
@ -1085,10 +1085,10 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
def row(self, *args): def row(self, *args):
""" """
Adds a list of InlineKeyboardButton to the keyboard. Adds a list of InlineKeyboardButton to the keyboard.
This method does not consider row_width. This method does not consider row_width.
InlineKeyboardMarkup.row("A").row("B", "C").to_json() outputs: InlineKeyboardMarkup.row("A").row("B", "C").to_json() outputs:
'{keyboard: [["A"], ["B", "C"]]}' '{keyboard: [["A"], ["B", "C"]]}'
See https://core.telegram.org/bots/api#inlinekeyboardmarkup See https://core.telegram.org/bots/api#inlinekeyboardmarkup
:param args: Array of InlineKeyboardButton to append to the keyboard :param args: Array of InlineKeyboardButton to append to the keyboard
@ -1100,7 +1100,7 @@ class InlineKeyboardMarkup(Dictionaryable, JsonSerializable, JsonDeserializable)
def to_json(self): def to_json(self):
""" """
Converts this object to its json representation Converts this object to its json representation
following the Telegram API guidelines described here: following the Telegram API guidelines described here:
https://core.telegram.org/bots/api#inlinekeyboardmarkup https://core.telegram.org/bots/api#inlinekeyboardmarkup
:return: :return:
""" """

View File

@ -283,7 +283,7 @@ def split_string(text: str, chars_per_string: int) -> List[str]:
def smart_split(text: str, chars_per_string: int=MAX_MESSAGE_LENGTH) -> List[str]: def smart_split(text: str, chars_per_string: int=MAX_MESSAGE_LENGTH) -> List[str]:
""" r"""
Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string. Splits one string into multiple strings, with a maximum amount of `chars_per_string` characters per string.
This is very useful for splitting one giant message into multiples. This is very useful for splitting one giant message into multiples.
If `chars_per_string` > 4096: `chars_per_string` = 4096. If `chars_per_string` > 4096: `chars_per_string` = 4096.
@ -350,24 +350,27 @@ def quick_markup(values: Dict[str, Dict[str, Any]], row_width: int=2) -> types.I
This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)' This is useful to avoid always typing 'btn1 = InlineKeyboardButton(...)' 'btn2 = InlineKeyboardButton(...)'
Example: Example:
quick_markup({
'Twitter': {'url': 'https://twitter.com'},
'Facebook': {'url': 'https://facebook.com'},
'Back': {'callback_data': 'whatever'}
}, row_width=2):
returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
and a back button below
kwargs can be: .. code-block:: python
{
'url': None, quick_markup({
'callback_data': None, 'Twitter': {'url': 'https://twitter.com'},
'switch_inline_query': None, 'Facebook': {'url': 'https://facebook.com'},
'switch_inline_query_current_chat': None, 'Back': {'callback_data': 'whatever'}
'callback_game': None, }, row_width=2):
'pay': None, # returns an InlineKeyboardMarkup with two buttons in a row, one leading to Twitter, the other to facebook
'login_url': None # and a back button below
}
# kwargs can be:
{
'url': None,
'callback_data': None,
'switch_inline_query': None,
'switch_inline_query_current_chat': None,
'callback_game': None,
'pay': None,
'login_url': None
}
:param values: a dict containing all buttons to create in this format: {text: kwargs} {str:} :param values: a dict containing all buttons to create in this format: {text: kwargs} {str:}
:param row_width: int row width :param row_width: int row width
@ -484,12 +487,17 @@ def antiflood(function, *args, **kwargs):
""" """
Use this function inside loops in order to avoid getting TooManyRequests error. Use this function inside loops in order to avoid getting TooManyRequests error.
Example: Example:
from telebot.util import antiflood .. code-block:: python3
for chat_id in chat_id_list:
from telebot.util import antiflood
for chat_id in chat_id_list:
msg = antiflood(bot.send_message, chat_id, text) msg = antiflood(bot.send_message, chat_id, text)
You want get the :param function:
:param args:
:param kwargs:
:return: None
""" """
from telebot.apihelper import ApiTelegramException from telebot.apihelper import ApiTelegramException
from time import sleep from time import sleep