Passed
Push — master ( 879c1c...3da0e6 )
by torrua
01:45
created

keyboa.functions.get_callback_data()   B

Complexity

Conditions 7

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 22
nop 3
dl 0
loc 32
ccs 16
cts 16
cp 1
crap 7
rs 7.952
c 0
b 0
f 0
1
# -*- coding:utf-8 -*-
2 1
"""
3
Module for small service functions
4
"""
5
6 1
from typing import Optional, Iterable
7
8 1
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
9
10 1
from keyboa.constants import InlineButtonData, button_text_types, \
11
    ButtonText, callback_data_types, CallbackDataMarker, \
12
    BlockItems, MAXIMUM_ITEMS_IN_KEYBOARD, MAXIMUM_ITEMS_IN_LINE, \
13
    MINIMUM_ITEMS_IN_LINE, AUTO_ALIGNMENT_RANGE, MAXIMUM_CBD_LENGTH
14
15
16 1
def _keyboa_pre_check(
17
        items: BlockItems = None,
18
        items_in_row: int = None,
19
        keyboard: InlineKeyboardMarkup = None) -> None:
20
    """
21
    This function checks whether the keyboard parameters are beyond Telegram limits or not.
22
23
    :param items: InlineRowItems - Sequence of elements with optional structure,
24
        where each top-level item will be a row with one or several buttons.
25
26
    :param items_in_row: int - Desired number of buttons in one row. Should be from 1 to 8.
27
        Optional. The default value is None.
28
29
    :param keyboard: InlineKeyboardMarkup object to which we will attach the list items.
30
        We need to count the existing buttons so as not to go beyond the general limits.
31
        Optional. The default value is None.
32
33
    :return: None if everything is okay.
34
35
    Covered by tests.
36
    """
37
38 1
    if items is None:
39 1
        return
40
41 1
    if items and not isinstance(items, list):
42 1
        items = [items, ]
43
44 1
    if keyboard and not isinstance(keyboard, InlineKeyboardMarkup):
45 1
        type_error_message = \
46
            "Keyboard to which the new items will be added " \
47
            "should have InlineKeyboardMarkup type. Now it is a %s" % type(keyboard)
48 1
        raise TypeError(type_error_message)
49
50
    # We need to count existing buttons too if we passed keyboard object to the function
51 1
    items_in_keyboard = get_total_items_number(items, keyboard)
52 1
    check_keyboard_items_limits(items_in_keyboard, items_in_row)
53
54
55 1
def check_keyboard_items_limits(items_in_keyboard: int, items_in_row: int) -> None:
56
    """
57
    :param items_in_keyboard:
58
    :param items_in_row:
59
    :return:
60
    """
61
62 1
    if items_in_keyboard > MAXIMUM_ITEMS_IN_KEYBOARD:
63 1
        value_error_message_keyboard = \
64
            "Telegram Bot API limit exceeded: The keyboard should have " \
65
            "from 1 to %s buttons at all. Your total amount is %s."
66 1
        raise ValueError(value_error_message_keyboard %
67
                         (MAXIMUM_ITEMS_IN_KEYBOARD, items_in_keyboard))
68
69 1
    if items_in_row is not None and \
70
            (MINIMUM_ITEMS_IN_LINE > items_in_row or items_in_row > MAXIMUM_ITEMS_IN_LINE):
71 1
        value_error_message_line = \
72
            "Telegram Bot API limit exceeded: " \
73
            "The keyboard line should have from 1 to %s buttons. You entered %s."
74 1
        raise ValueError(value_error_message_line %
75
                         (MAXIMUM_ITEMS_IN_LINE, items_in_row))
76
77
78 1
def get_total_items_number(items, keyboard) -> int:
79
    """
80
    :param items:
81
    :param keyboard:
82
    :return:
83
    """
84 1
    total_items_number = sum(
85
        len(row) if isinstance(row, (list, tuple, set)) else 1 for row in items)
86
87 1
    if not keyboard:
88 1
        return total_items_number
89
90 1
    keyboard_items = keyboard.__dict__['keyboard']
91 1
    current_keyboard_items_number = sum(len(row) for row in keyboard_items)
92 1
    return total_items_number + current_keyboard_items_number
93
94
95 1
def get_callback_data(
96
        raw_callback: CallbackDataMarker,
97
        front_marker: CallbackDataMarker = str(),
98
        back_marker: CallbackDataMarker = str()) -> str:
99
    """
100
    :param raw_callback:
101
    :param front_marker:
102
    :param back_marker:
103
    :return:
104
    """
105 1
    if front_marker is None:
106 1
        front_marker = str()
107 1
    if back_marker is None:
108 1
        back_marker = str()
109 1
    for marker in (front_marker, back_marker):
110 1
        if not isinstance(marker, callback_data_types):
111 1
            type_error_message = \
112
                "Marker could not have %s type. Only %s allowed." \
113
                % (type(marker), CallbackDataMarker)
114 1
            raise TypeError(type_error_message)
115 1
    callback_data = "%s%s%s" % (front_marker, raw_callback, back_marker)
116
117 1
    if not callback_data:
118 1
        raise ValueError("The callback data cannot be empty.")
119
120 1
    if len(callback_data.encode()) > MAXIMUM_CBD_LENGTH:
121 1
        size_error_message = "The callback data cannot be more than " \
122
                             "64 bytes for one button. Your size is %s" \
123
                             % len(callback_data.encode())
124 1
        raise ValueError(size_error_message)
125
126 1
    return callback_data
127
128
129 1
def get_callback(button_data: tuple) -> str:
130
    """
131
    :param button_data:
132
    :return:
133
    """
134 1
    callback = button_data[1]
135 1
    if not isinstance(callback, callback_data_types):
136 1
        type_error_message = "Callback cannot be %s. Only %s allowed." \
137
                             % (type(callback), callback_data_types)
138 1
        raise TypeError(type_error_message)
139 1
    return callback
140
141
142 1
def get_text(button_data: tuple) -> str:
143
    """
144
    :param button_data:
145
    :return:
146
    """
147 1
    raw_text = button_data[0]
148 1
    if not isinstance(raw_text, button_text_types):
149 1
        type_error_message = "Button text cannot be %s. Only %s allowed." \
150
                             % (type(raw_text), ButtonText)
151 1
        raise TypeError(type_error_message)
152 1
    text = str(raw_text)
153 1
    if not text:
154 1
        raise ValueError("Button text cannot be empty.")
155 1
    return text
156
157
158 1
def get_button_tuple(button_data: InlineButtonData, copy_text_to_callback: bool) -> tuple:
159
    """
160
    :param button_data:
161
    :param copy_text_to_callback:
162
    :return:
163
    """
164 1
    if not isinstance(button_data, (tuple, dict, str, int)):
165 1
        type_error_message = \
166
            "Cannot create %s from %s. Please use %s instead.\n" \
167
            "Probably you specified 'auto_alignment' or 'items_in_line' " \
168
            "parameter for StructuredSequence." \
169
            % (InlineKeyboardButton, type(button_data), InlineButtonData)
170 1
        raise TypeError(type_error_message)
171 1
    if isinstance(button_data, (str, int)):
172 1
        btn_tuple = button_data, button_data if copy_text_to_callback else str()
173
174 1
    elif isinstance(button_data, dict):
175 1
        if len(button_data.keys()) != 1:
176 1
            value_type_error = \
177
                "Cannot convert dictionary to InlineButtonData object. " \
178
                "You passed more than one item, but did not add 'text' key."
179 1
            raise ValueError(value_type_error)
180
181 1
        btn_tuple = next(iter(button_data.items()))
182
    else:
183 1
        btn_tuple = button_data
184
185 1
    if len(btn_tuple) == 1 or btn_tuple[1] is None:
186 1
        btn_tuple = btn_tuple[0], btn_tuple[0] if copy_text_to_callback else str()
187 1
    return btn_tuple
188
189
190 1
def calculate_items_in_row(items, auto_alignment, reverse_alignment_range) -> Optional[int]:
191
    """
192
    :param items:
193
    :param auto_alignment:
194
    :param reverse_alignment_range:
195
    :return:
196
    """
197
198 1
    items_in_row = None
199 1
    alignment_range = get_alignment_range(auto_alignment)
200
201 1
    if reverse_alignment_range:
202 1
        alignment_range = reversed(alignment_range)
203
204 1
    for divider in alignment_range:
205 1
        if not len(items) % divider:
206 1
            items_in_row = divider
207 1
            break
208
209 1
    return items_in_row
210
211
212 1
def get_alignment_range(auto_alignment):
213
    """
214
    :param auto_alignment:
215
    :return:
216
    """
217
218 1
    if isinstance(auto_alignment, bool):
219 1
        return AUTO_ALIGNMENT_RANGE
220
221 1
    if not (isinstance(auto_alignment, Iterable)
222
            and all(map(lambda s: isinstance(s, int), auto_alignment))):
223 1
        type_error_message = \
224
            "The auto_alignment variable has not a proper type. " \
225
            "Only Iterable of integers or boolean type allowed.\n" \
226
            "You may define it as 'True' to use AUTO_ALIGNMENT_RANGE."
227 1
        raise TypeError(type_error_message)
228
229 1
    if max(auto_alignment) > MAXIMUM_ITEMS_IN_LINE \
230
            or min(auto_alignment) < MINIMUM_ITEMS_IN_LINE:
231 1
        value_error_message = \
232
            "The auto_alignment's item values should be between " \
233
            "%s and %s. You entered: %s\n" \
234
            "You may define it as 'True' to use AUTO_ALIGNMENT_RANGE." \
235
            % (MINIMUM_ITEMS_IN_LINE, MAXIMUM_ITEMS_IN_LINE, auto_alignment)
236 1
        raise ValueError(value_error_message)
237
238
    return auto_alignment
239