validators.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. # ©️ Dan Gazizullin, 2021-2022
  2. # This file is a part of Hikka Userbot
  3. # 🌐 https://github.com/hikariatama/Hikka
  4. # You can redistribute it and/or modify it under the terms of the GNU AGPLv3
  5. # 🔑 https://www.gnu.org/licenses/agpl-3.0.html
  6. import functools
  7. import re
  8. import typing
  9. import grapheme
  10. from emoji import get_emoji_unicode_dict
  11. from . import utils
  12. ConfigAllowedTypes = typing.Union[tuple, list, str, int, bool, None]
  13. ALLOWED_EMOJIS = set(get_emoji_unicode_dict("en").values())
  14. class ValidationError(Exception):
  15. """
  16. Is being raised when config value passed can't be converted properly
  17. Must be raised with string, describing why value is incorrect
  18. It will be shown in .config, if user tries to set incorrect value
  19. """
  20. class Validator:
  21. """
  22. Class used as validator of config value
  23. :param validator: Sync function, which raises `ValidationError` if passed
  24. value is incorrect (with explanation) and returns converted
  25. value if it is semantically correct.
  26. ⚠️ If validator returns `None`, value will always be set to `None`
  27. :param doc: Docstrings for this validator as string, or dict in format:
  28. {
  29. "en": "docstring",
  30. "ru": "докстрингом",
  31. "it": "docstring",
  32. "de": "Dokumentation",
  33. "tr": "dökümantasyon",
  34. "uz": "hujjat",
  35. "es": "documentación",
  36. "kk": "құжат",
  37. }
  38. Use instrumental case with lowercase
  39. :param _internal_id: Do not pass anything here, or things will break
  40. """
  41. def __init__(
  42. self,
  43. validator: callable,
  44. doc: typing.Optional[typing.Union[str, dict]] = None,
  45. _internal_id: typing.Optional[int] = None,
  46. ):
  47. self.validate = validator
  48. if isinstance(doc, str):
  49. doc = {"en": doc, "ru": doc, "it": doc, "de": doc, "tr": doc, "uz": doc}
  50. self.doc = doc
  51. self.internal_id = _internal_id
  52. class Boolean(Validator):
  53. """
  54. Any logical value to be passed
  55. `1`, `"1"` etc. will be automatically converted to bool
  56. """
  57. def __init__(self):
  58. super().__init__(
  59. self._validate,
  60. {
  61. "en": "boolean",
  62. "ru": "логическим значением",
  63. "it": "booleano",
  64. "de": "logischen Wert",
  65. "tr": "mantıksal değer",
  66. "uz": "mantiqiy qiymat",
  67. "es": "valor lógico",
  68. "kk": "логикалық мән",
  69. },
  70. _internal_id="Boolean",
  71. )
  72. @staticmethod
  73. def _validate(value: ConfigAllowedTypes, /) -> bool:
  74. true = ["True", "true", "1", 1, True, "yes", "Yes", "on", "On", "y", "Y"]
  75. false = ["False", "false", "0", 0, False, "no", "No", "off", "Off", "n", "N"]
  76. if value not in true + false:
  77. raise ValidationError("Passed value must be a boolean")
  78. return value in true
  79. class Integer(Validator):
  80. """
  81. Checks whether passed argument is an integer value
  82. :param digits: Digits quantity, which must be passed
  83. :param minimum: Minimal number to be passed
  84. :param maximum: Maximum number to be passed
  85. """
  86. def __init__(
  87. self,
  88. *,
  89. digits: typing.Optional[int] = None,
  90. minimum: typing.Optional[int] = None,
  91. maximum: typing.Optional[int] = None,
  92. ):
  93. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  94. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  95. _sign_it = "positivo " if minimum is not None and minimum == 0 else ""
  96. _sign_de = "positiv " if minimum is not None and minimum == 0 else ""
  97. _sign_tr = "pozitif " if minimum is not None and minimum == 0 else ""
  98. _sign_uz = "musbat " if minimum is not None and minimum == 0 else ""
  99. _sign_es = "positivo " if minimum is not None and minimum == 0 else ""
  100. _sign_kk = "мәндік " if minimum is not None and minimum == 0 else ""
  101. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  102. _sign_ru = (
  103. "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  104. )
  105. _sign_it = "negativo " if maximum is not None and maximum == 0 else _sign_it
  106. _sign_de = "negativ " if maximum is not None and maximum == 0 else _sign_de
  107. _sign_tr = "negatif " if maximum is not None and maximum == 0 else _sign_tr
  108. _sign_uz = "manfiy " if maximum is not None and maximum == 0 else _sign_uz
  109. _sign_es = "negativo " if maximum is not None and maximum == 0 else _sign_es
  110. _sign_kk = "мәнсіздік " if maximum is not None and maximum == 0 else _sign_kk
  111. _digits_en = f" with exactly {digits} digits" if digits is not None else ""
  112. _digits_ru = f", в котором ровно {digits} цифр " if digits is not None else ""
  113. _digits_it = f" con esattamente {digits} cifre" if digits is not None else ""
  114. _digits_de = f" mit genau {digits} Ziffern" if digits is not None else ""
  115. _digits_tr = f" tam olarak {digits} basamaklı" if digits is not None else ""
  116. _digits_uz = f" to'g'ri {digits} raqamlar bilan" if digits is not None else ""
  117. _digits_es = f" con exactamente {digits} dígitos" if digits is not None else ""
  118. _digits_kk = f" тең {digits} сандық" if digits is not None else ""
  119. if minimum is not None and minimum != 0:
  120. doc = (
  121. {
  122. "en": f"{_sign_en}integer greater than {minimum}{_digits_en}",
  123. "ru": f"{_sign_ru}целым числом больше {minimum}{_digits_ru}",
  124. "it": f"{_sign_it}intero maggiore di {minimum}{_digits_it}",
  125. "de": f"{_sign_de}ganze Zahl größer als {minimum}{_digits_de}",
  126. "tr": f"{_sign_tr}tam sayı {minimum} den büyük{_digits_tr}",
  127. "uz": f"{_sign_uz}butun son {minimum} dan katta{_digits_uz}",
  128. "es": f"{_sign_es}número entero mayor que {minimum}{_digits_es}",
  129. "kk": f"{_sign_kk}толық сан {minimum} тан көп{_digits_kk}",
  130. }
  131. if maximum is None and maximum != 0
  132. else {
  133. "en": f"{_sign_en}integer from {minimum} to {maximum}{_digits_en}",
  134. "ru": (
  135. f"{_sign_ru}целым числом в промежутке от {minimum} до"
  136. f" {maximum}{_digits_ru}"
  137. ),
  138. "it": (
  139. f"{_sign_it}intero compreso tra {minimum} e {maximum}"
  140. f"{_digits_it}"
  141. ),
  142. "de": (
  143. f"{_sign_de}ganze Zahl von {minimum} bis {maximum}{_digits_de}"
  144. ),
  145. "tr": (
  146. f"{_sign_tr}tam sayı {minimum} ile {maximum} arasında"
  147. f"{_digits_tr}"
  148. ),
  149. "uz": (
  150. f"{_sign_uz}butun son {minimum} dan {maximum} gacha{_digits_uz}"
  151. ),
  152. "es": (
  153. f"{_sign_es}número entero de {minimum} a {maximum}{_digits_es}"
  154. ),
  155. "kk": (
  156. f"{_sign_kk}толық сан {minimum} ден {maximum} қарай{_digits_kk}"
  157. ),
  158. }
  159. )
  160. elif maximum is None and maximum != 0:
  161. doc = {
  162. "en": f"{_sign_en}integer{_digits_en}",
  163. "ru": f"{_sign_ru}целым числом{_digits_ru}",
  164. "it": f"{_sign_it}intero{_digits_it}",
  165. "de": f"{_sign_de}ganze Zahl{_digits_de}",
  166. "tr": f"{_sign_tr}tam sayı{_digits_tr}",
  167. "uz": f"{_sign_uz}butun son{_digits_uz}",
  168. "es": f"{_sign_es}número entero{_digits_es}",
  169. "kk": f"{_sign_kk}толық сан{_digits_kk}",
  170. }
  171. else:
  172. doc = {
  173. "en": f"{_sign_en}integer less than {maximum}{_digits_en}",
  174. "ru": f"{_sign_ru}целым числом меньше {maximum}{_digits_ru}",
  175. "it": f"{_sign_it}intero minore di {maximum}{_digits_it}",
  176. "de": f"{_sign_de}ganze Zahl kleiner als {maximum}{_digits_de}",
  177. "tr": f"{_sign_tr}tam sayı {maximum} den küçük{_digits_tr}",
  178. "uz": f"{_sign_uz}butun son {maximum} dan kichik{_digits_uz}",
  179. "es": f"{_sign_es}número entero menor que {maximum}{_digits_es}",
  180. "kk": f"{_sign_kk}толық сан {maximum} тан кем{_digits_kk}",
  181. }
  182. super().__init__(
  183. functools.partial(
  184. self._validate,
  185. digits=digits,
  186. minimum=minimum,
  187. maximum=maximum,
  188. ),
  189. doc,
  190. _internal_id="Integer",
  191. )
  192. @staticmethod
  193. def _validate(
  194. value: ConfigAllowedTypes,
  195. /,
  196. *,
  197. digits: int,
  198. minimum: int,
  199. maximum: int,
  200. ) -> typing.Union[int, None]:
  201. try:
  202. value = int(str(value).strip())
  203. except ValueError:
  204. raise ValidationError(f"Passed value ({value}) must be a number")
  205. if minimum is not None and value < minimum:
  206. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  207. if maximum is not None and value > maximum:
  208. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  209. if digits is not None and len(str(value)) != digits:
  210. raise ValidationError(
  211. f"The length of passed value ({value}) is incorrect "
  212. f"(Must be exactly {digits} digits)"
  213. )
  214. return value
  215. class Choice(Validator):
  216. """
  217. Check whether entered value is in the allowed list
  218. :param possible_values: Allowed values to be passed to config param
  219. """
  220. def __init__(
  221. self,
  222. possible_values: typing.List[ConfigAllowedTypes],
  223. /,
  224. ):
  225. possible = " / ".join(list(map(str, possible_values)))
  226. super().__init__(
  227. functools.partial(self._validate, possible_values=possible_values),
  228. {
  229. "en": f"one of the following: {possible}",
  230. "ru": f"одним из: {possible}",
  231. "it": f"uno dei seguenti: {possible}",
  232. "de": f"einer der folgenden: {possible}",
  233. "tr": f"şunlardan biri: {possible}",
  234. "uz": f"quyidagilardan biri: {possible}",
  235. "es": f"uno de los siguientes: {possible}",
  236. "kk": f"келесілердің бірі: {possible}",
  237. },
  238. _internal_id="Choice",
  239. )
  240. @staticmethod
  241. def _validate(
  242. value: ConfigAllowedTypes,
  243. /,
  244. *,
  245. possible_values: typing.List[ConfigAllowedTypes],
  246. ) -> ConfigAllowedTypes:
  247. if value not in possible_values:
  248. raise ValidationError(
  249. f"Passed value ({value}) is not one of the following:"
  250. f" {' / '.join(list(map(str, possible_values)))}"
  251. )
  252. return value
  253. class MultiChoice(Validator):
  254. """
  255. Check whether every entered value is in the allowed list
  256. :param possible_values: Allowed values to be passed to config param
  257. """
  258. def __init__(
  259. self,
  260. possible_values: typing.List[ConfigAllowedTypes],
  261. /,
  262. ):
  263. possible = " / ".join(list(map(str, possible_values)))
  264. super().__init__(
  265. functools.partial(self._validate, possible_values=possible_values),
  266. {
  267. "en": f"list of values, where each one must be one of: {possible}",
  268. "ru": (
  269. "список значений, каждое из которых должно быть одним из"
  270. f" следующего: {possible}"
  271. ),
  272. "it": (
  273. "elenco di valori, ognuno dei quali deve essere uno dei"
  274. f" seguenti: {possible}"
  275. ),
  276. "de": (
  277. "Liste von Werten, bei denen jeder einer der folgenden sein muss:"
  278. f" {possible}"
  279. ),
  280. "tr": (
  281. "değerlerin listesi, her birinin şunlardan biri olması gerekir:"
  282. f" {possible}"
  283. ),
  284. "uz": (
  285. "qiymatlar ro'yxati, har biri quyidagilardan biri bo'lishi kerak:"
  286. f" {possible}"
  287. ),
  288. "es": f"lista de valores, donde cada uno debe ser uno de: {possible}",
  289. "kk": (
  290. "мәндер тізімі, әрбірінің келесілердің бірі болуы керек:"
  291. f" {possible}"
  292. ),
  293. },
  294. _internal_id="MultiChoice",
  295. )
  296. @staticmethod
  297. def _validate(
  298. value: typing.List[ConfigAllowedTypes],
  299. /,
  300. *,
  301. possible_values: typing.List[ConfigAllowedTypes],
  302. ) -> typing.List[ConfigAllowedTypes]:
  303. if not isinstance(value, (list, tuple)):
  304. value = [value]
  305. for item in value:
  306. if item not in possible_values:
  307. raise ValidationError(
  308. f"One of passed values ({item}) is not one of the following:"
  309. f" {' / '.join(list(map(str, possible_values)))}"
  310. )
  311. return list(set(value))
  312. class Series(Validator):
  313. """
  314. Represents the series of value (simply `list`)
  315. :param separator: With which separator values must be separated
  316. :param validator: Internal validator for each sequence value
  317. :param min_len: Minimal number of series items to be passed
  318. :param max_len: Maximum number of series items to be passed
  319. :param fixed_len: Fixed number of series items to be passed
  320. """
  321. def __init__(
  322. self,
  323. validator: typing.Optional[Validator] = None,
  324. min_len: typing.Optional[int] = None,
  325. max_len: typing.Optional[int] = None,
  326. fixed_len: typing.Optional[int] = None,
  327. ):
  328. def trans(lang: str) -> str:
  329. return validator.doc.get(lang, validator.doc["en"])
  330. _each_en = f" (each must be {trans('en')})" if validator is not None else ""
  331. _each_ru = (
  332. f" (каждое должно быть {trans('ru')})" if validator is not None else ""
  333. )
  334. _each_it = (
  335. f" (ognuno deve essere {trans('it')})" if validator is not None else ""
  336. )
  337. _each_de = f" (jedes muss {trans('de')})" if validator is not None else ""
  338. _each_tr = f" (her biri {trans('tr')})" if validator is not None else ""
  339. _each_uz = f" (har biri {trans('uz')})" if validator is not None else ""
  340. _each_es = f" (cada uno {trans('es')})" if validator is not None else ""
  341. _each_kk = f" (әрбірі {trans('kk')})" if validator is not None else ""
  342. if fixed_len is not None:
  343. _len_en = f" (exactly {fixed_len} pcs.)"
  344. _len_ru = f" (ровно {fixed_len} шт.)"
  345. _len_it = f" (esattamente {fixed_len} pezzi)"
  346. _len_de = f" (genau {fixed_len} Stück)"
  347. _len_tr = f" (tam olarak {fixed_len} adet)"
  348. _len_uz = f" (to'g'ri {fixed_len} ta)"
  349. _len_es = f" (exactamente {fixed_len} piezas)"
  350. _len_kk = f" (тоғыз {fixed_len} құны)"
  351. elif min_len is None:
  352. if max_len is None:
  353. _len_en = ""
  354. _len_ru = ""
  355. _len_it = ""
  356. _len_de = ""
  357. _len_tr = ""
  358. _len_uz = ""
  359. _len_es = ""
  360. _len_kk = ""
  361. else:
  362. _len_en = f" (up to {max_len} pcs.)"
  363. _len_ru = f" (до {max_len} шт.)"
  364. _len_it = f" (fino a {max_len} pezzi)"
  365. _len_de = f" (bis zu {max_len} Stück)"
  366. _len_tr = f" (en fazla {max_len} adet)"
  367. _len_uz = f" (eng ko'p {max_len} ta)"
  368. _len_es = f" (hasta {max_len} piezas)"
  369. _len_kk = f" (көптегенде {max_len} құны)"
  370. elif max_len is not None:
  371. _len_en = f" (from {min_len} to {max_len} pcs.)"
  372. _len_ru = f" (от {min_len} до {max_len} шт.)"
  373. _len_it = f" (da {min_len} a {max_len} pezzi)"
  374. _len_de = f" (von {min_len} bis {max_len} Stück)"
  375. _len_tr = f" ({min_len} ile {max_len} arasında {max_len} adet)"
  376. _len_uz = f" ({min_len} dan {max_len} gacha {max_len} ta)"
  377. _len_es = f" (desde {min_len} hasta {max_len} piezas)"
  378. _len_kk = f" ({min_len} ден {max_len} ге {max_len} құны)"
  379. else:
  380. _len_en = f" (at least {min_len} pcs.)"
  381. _len_ru = f" (как минимум {min_len} шт.)"
  382. _len_it = f" (almeno {min_len} pezzi)"
  383. _len_de = f" (mindestens {min_len} Stück)"
  384. _len_tr = f" (en az {min_len} adet)"
  385. _len_uz = f" (kamida {min_len} ta)"
  386. _len_es = f" (al menos {min_len} piezas)"
  387. _len_kk = f" (кем дегенде {min_len} құны)"
  388. super().__init__(
  389. functools.partial(
  390. self._validate,
  391. validator=validator,
  392. min_len=min_len,
  393. max_len=max_len,
  394. fixed_len=fixed_len,
  395. ),
  396. {
  397. "en": f"series of values{_len_en}{_each_en}, separated with «,»",
  398. "ru": f"списком значений{_len_ru}{_each_ru}, разделенных «,»",
  399. "it": f"serie di valori{_len_it}{_each_it}, separati con «,»",
  400. "de": f"Liste von Werten{_len_de}{_each_de}, getrennt mit «,»",
  401. "tr": f"değerlerin listesi{_len_tr}{_each_tr}, «,» ile ayrılmış",
  402. "uz": f"qiymatlar ro'yxati{_len_uz}{_each_uz}, «,» bilan ajratilgan",
  403. "es": f"lista de valores{_len_es}{_each_es}, separados con «,»",
  404. "kk": f"мәндер тізімі{_len_kk}{_each_kk}, «,» бойынша бөлінген",
  405. },
  406. _internal_id="Series",
  407. )
  408. @staticmethod
  409. def _validate(
  410. value: ConfigAllowedTypes,
  411. /,
  412. *,
  413. validator: typing.Optional[Validator] = None,
  414. min_len: typing.Optional[int] = None,
  415. max_len: typing.Optional[int] = None,
  416. fixed_len: typing.Optional[int] = None,
  417. ) -> typing.List[ConfigAllowedTypes]:
  418. if not isinstance(value, (list, tuple, set)):
  419. value = str(value).split(",")
  420. if isinstance(value, (tuple, set)):
  421. value = list(value)
  422. if min_len is not None and len(value) < min_len:
  423. raise ValidationError(
  424. f"Passed value ({value}) contains less than {min_len} items"
  425. )
  426. if max_len is not None and len(value) > max_len:
  427. raise ValidationError(
  428. f"Passed value ({value}) contains more than {max_len} items"
  429. )
  430. if fixed_len is not None and len(value) != fixed_len:
  431. raise ValidationError(
  432. f"Passed value ({value}) must contain exactly {fixed_len} items"
  433. )
  434. value = [item.strip() if isinstance(item, str) else item for item in value]
  435. if isinstance(validator, Validator):
  436. for i, item in enumerate(value):
  437. try:
  438. value[i] = validator.validate(item)
  439. except ValidationError:
  440. raise ValidationError(
  441. f"Passed value ({value}) contains invalid item"
  442. f" ({str(item).strip()}), which must be {validator.doc['en']}"
  443. )
  444. value = list(filter(lambda x: x, value))
  445. return value
  446. class Link(Validator):
  447. """Valid url must be specified"""
  448. def __init__(self):
  449. super().__init__(
  450. lambda value: self._validate(value),
  451. {
  452. "en": "link",
  453. "ru": "ссылкой",
  454. "it": "collegamento",
  455. "de": "Link",
  456. "tr": "bağlantı",
  457. "uz": "havola",
  458. "es": "enlace",
  459. "kk": "сілтеме",
  460. },
  461. _internal_id="Link",
  462. )
  463. @staticmethod
  464. def _validate(value: ConfigAllowedTypes, /) -> str:
  465. try:
  466. if not utils.check_url(value):
  467. raise Exception("Invalid URL")
  468. except Exception:
  469. raise ValidationError(f"Passed value ({value}) is not a valid URL")
  470. return value
  471. class String(Validator):
  472. """
  473. Checks for length of passed value and automatically converts it to string
  474. :param length: Exact length of string
  475. :param min_len: Minimal length of string
  476. :param max_len: Maximum length of string
  477. """
  478. def __init__(
  479. self,
  480. length: typing.Optional[int] = None,
  481. min_len: typing.Optional[int] = None,
  482. max_len: typing.Optional[int] = None,
  483. ):
  484. if length is not None:
  485. doc = {
  486. "en": f"string of length {length}",
  487. "ru": f"строкой из {length} символа(-ов)",
  488. "it": f"stringa di lunghezza {length}",
  489. "de": f"Zeichenkette mit Länge {length}",
  490. "tr": f"{length} karakter uzunluğunda dize",
  491. "uz": f"{length} ta belgi uzunlig'ida satr",
  492. "es": f"cadena de longitud {length}",
  493. "kk": f"{length} ұзындығында сөз",
  494. }
  495. else:
  496. if min_len is None:
  497. if max_len is None:
  498. doc = {
  499. "en": "string",
  500. "ru": "строкой",
  501. "it": "stringa",
  502. "de": "Zeichenkette",
  503. "tr": "dize",
  504. "uz": "satr",
  505. "es": "cadena",
  506. "kk": "сөз",
  507. }
  508. else:
  509. doc = {
  510. "en": f"string of length up to {max_len}",
  511. "ru": f"строкой не более чем из {max_len} символа(-ов)",
  512. "it": f"stringa di lunghezza massima {max_len}",
  513. "de": f"Zeichenkette mit Länge bis zu {max_len}",
  514. "tr": f"{max_len} karakter uzunluğunda dize",
  515. "uz": f"{max_len} ta belgi uzunlig'ida satr",
  516. "es": f"cadena de longitud {max_len}",
  517. "kk": f"{max_len} ұзындығында сөз",
  518. }
  519. elif max_len is not None:
  520. doc = {
  521. "en": f"string of length from {min_len} to {max_len}",
  522. "ru": f"строкой из {min_len}-{max_len} символа(-ов)",
  523. "it": f"stringa di lunghezza da {min_len} a {max_len}",
  524. "de": f"Zeichenkette mit Länge von {min_len} bis {max_len}",
  525. "tr": f"{min_len}-{max_len} karakter uzunluğunda dize",
  526. "uz": f"{min_len}-{max_len} ta belgi uzunlig'ida satr",
  527. "es": f"cadena de longitud {min_len}-{max_len}",
  528. "kk": f"{min_len}-{max_len} ұзындығында сөз",
  529. }
  530. else:
  531. doc = {
  532. "en": f"string of length at least {min_len}",
  533. "ru": f"строкой не менее чем из {min_len} символа(-ов)",
  534. "it": f"stringa di lunghezza minima {min_len}",
  535. "de": f"Zeichenkette mit Länge mindestens {min_len}",
  536. "tr": f"{min_len} karakter uzunluğunda dize",
  537. "uz": f"{min_len} ta belgi uzunlig'ida satr",
  538. "es": f"cadena de longitud {min_len}",
  539. "kk": f"{min_len} ұзындығында сөз",
  540. }
  541. super().__init__(
  542. functools.partial(
  543. self._validate,
  544. length=length,
  545. min_len=min_len,
  546. max_len=max_len,
  547. ),
  548. doc,
  549. _internal_id="String",
  550. )
  551. @staticmethod
  552. def _validate(
  553. value: ConfigAllowedTypes,
  554. /,
  555. *,
  556. length: typing.Optional[int],
  557. min_len: typing.Optional[int],
  558. max_len: typing.Optional[int],
  559. ) -> str:
  560. if (
  561. isinstance(length, int)
  562. and len(list(grapheme.graphemes(str(value)))) != length
  563. ):
  564. raise ValidationError(
  565. f"Passed value ({value}) must be a length of {length}"
  566. )
  567. if (
  568. isinstance(min_len, int)
  569. and len(list(grapheme.graphemes(str(value)))) < min_len
  570. ):
  571. raise ValidationError(
  572. f"Passed value ({value}) must be a length of at least {min_len}"
  573. )
  574. if (
  575. isinstance(max_len, int)
  576. and len(list(grapheme.graphemes(str(value)))) > max_len
  577. ):
  578. raise ValidationError(
  579. f"Passed value ({value}) must be a length of up to {max_len}"
  580. )
  581. return str(value)
  582. class RegExp(Validator):
  583. """
  584. Checks if value matches the regex
  585. :param regex: Regex to match
  586. :param flags: Flags to pass to re.compile
  587. :param description: Description of regex
  588. """
  589. def __init__(
  590. self,
  591. regex: str,
  592. flags: typing.Optional[re.RegexFlag] = None,
  593. description: typing.Optional[typing.Union[dict, str]] = None,
  594. ):
  595. if not flags:
  596. flags = 0
  597. try:
  598. re.compile(regex, flags=flags)
  599. except re.error as e:
  600. raise Exception(f"{regex} is not a valid regex") from e
  601. if description is None:
  602. doc = {
  603. "en": f"string matching pattern «{regex}»",
  604. "ru": f"строкой, соответствующей шаблону «{regex}»",
  605. "it": f"stringa che corrisponde al modello «{regex}»",
  606. "de": f"Zeichenkette, die dem Muster «{regex}» entspricht",
  607. "tr": f"«{regex}» kalıbına uygun dize",
  608. "uz": f"«{regex}» shabloniga mos matn",
  609. "es": f"cadena que coincide con el patrón «{regex}»",
  610. "kk": f"«{regex}» үлгісіне сәйкес сөз",
  611. }
  612. else:
  613. if isinstance(description, str):
  614. doc = {"en": description}
  615. else:
  616. doc = description
  617. super().__init__(
  618. functools.partial(self._validate, regex=regex, flags=flags),
  619. doc,
  620. _internal_id="RegExp",
  621. )
  622. @staticmethod
  623. def _validate(
  624. value: ConfigAllowedTypes,
  625. /,
  626. *,
  627. regex: str,
  628. flags: typing.Optional[re.RegexFlag],
  629. ) -> str:
  630. if not re.match(regex, str(value), flags=flags):
  631. raise ValidationError(f"Passed value ({value}) must follow pattern {regex}")
  632. return str(value)
  633. class Float(Validator):
  634. """
  635. Checks whether passed argument is a float value
  636. :param minimum: Minimal number to be passed
  637. :param maximum: Maximum number to be passed
  638. """
  639. def __init__(
  640. self,
  641. minimum: typing.Optional[float] = None,
  642. maximum: typing.Optional[float] = None,
  643. ):
  644. _sign_en = "positive " if minimum is not None and minimum == 0 else ""
  645. _sign_ru = "положительным " if minimum is not None and minimum == 0 else ""
  646. _sign_it = "positivo " if minimum is not None and minimum == 0 else ""
  647. _sign_de = "positiv " if minimum is not None and minimum == 0 else ""
  648. _sign_tr = "pozitif " if minimum is not None and minimum == 0 else ""
  649. _sign_uz = "musbat " if minimum is not None and minimum == 0 else ""
  650. _sign_es = "positivo " if minimum is not None and minimum == 0 else ""
  651. _sign_kk = "мың " if minimum is not None and minimum == 0 else ""
  652. _sign_en = "negative " if maximum is not None and maximum == 0 else _sign_en
  653. _sign_ru = (
  654. "отрицательным " if maximum is not None and maximum == 0 else _sign_ru
  655. )
  656. _sign_it = "negativo " if maximum is not None and maximum == 0 else _sign_it
  657. _sign_de = "negativ " if maximum is not None and maximum == 0 else _sign_de
  658. _sign_tr = "negatif " if maximum is not None and maximum == 0 else _sign_tr
  659. _sign_uz = "manfiy " if maximum is not None and maximum == 0 else _sign_uz
  660. _sign_es = "negativo " if maximum is not None and maximum == 0 else _sign_es
  661. _sign_kk = "мінус " if maximum is not None and maximum == 0 else _sign_kk
  662. if minimum is not None and minimum != 0:
  663. doc = (
  664. {
  665. "en": f"{_sign_en}float greater than {minimum}",
  666. "ru": f"{_sign_ru}дробным числом больше {minimum}",
  667. "it": f"{_sign_it}numero decimale maggiore di {minimum}",
  668. "de": f"{_sign_de}Fließkommazahl größer als {minimum}",
  669. "tr": f"{_sign_tr}ondalık sayı {minimum} dan büyük",
  670. "uz": f"{_sign_uz}butun son {minimum} dan katta",
  671. "es": f"{_sign_es}número decimal mayor que {minimum}",
  672. "kk": f"{_sign_kk}сандық сан {minimum} тан аспау",
  673. }
  674. if maximum is None and maximum != 0
  675. else {
  676. "en": f"{_sign_en}float from {minimum} to {maximum}",
  677. "ru": (
  678. f"{_sign_ru}дробным числом в промежутке от {minimum} до"
  679. f" {maximum}"
  680. ),
  681. "it": (
  682. f"{_sign_it}numero decimale compreso tra {minimum} e {maximum}"
  683. ),
  684. "de": f"{_sign_de}Fließkommazahl von {minimum} bis {maximum}",
  685. "tr": f"{_sign_tr}ondalık sayı {minimum} ile {maximum} arasında",
  686. "uz": f"{_sign_uz}butun son {minimum} dan {maximum} gacha",
  687. "es": f"{_sign_es}número decimal de {minimum} a {maximum}",
  688. "kk": f"{_sign_kk}сандық сан {minimum} ден {maximum} ге",
  689. }
  690. )
  691. elif maximum is None and maximum != 0:
  692. doc = {
  693. "en": f"{_sign_en}float",
  694. "ru": f"{_sign_ru}дробным числом",
  695. "it": f"{_sign_it}numero decimale",
  696. "de": f"{_sign_de}Fließkommazahl",
  697. "tr": f"{_sign_tr}ondalık sayı",
  698. "uz": f"{_sign_uz}butun son",
  699. "es": f"{_sign_es}número decimal",
  700. "kk": f"{_sign_kk}сандық сан",
  701. }
  702. else:
  703. doc = {
  704. "en": f"{_sign_en}float less than {maximum}",
  705. "ru": f"{_sign_ru}дробным числом меньше {maximum}",
  706. "it": f"{_sign_it}numero decimale minore di {maximum}",
  707. "de": f"{_sign_de}Fließkommazahl kleiner als {maximum}",
  708. "tr": f"{_sign_tr}ondalık sayı {maximum} dan küçük",
  709. "uz": f"{_sign_uz}butun son {maximum} dan kichik",
  710. "es": f"{_sign_es}número decimal menor que {maximum}",
  711. "kk": f"{_sign_kk}сандық сан {maximum} тан кіші",
  712. }
  713. super().__init__(
  714. functools.partial(
  715. self._validate,
  716. minimum=minimum,
  717. maximum=maximum,
  718. ),
  719. doc,
  720. _internal_id="Float",
  721. )
  722. @staticmethod
  723. def _validate(
  724. value: ConfigAllowedTypes,
  725. /,
  726. *,
  727. minimum: typing.Optional[float] = None,
  728. maximum: typing.Optional[float] = None,
  729. ) -> float:
  730. try:
  731. value = float(str(value).strip().replace(",", "."))
  732. except ValueError:
  733. raise ValidationError(f"Passed value ({value}) must be a float")
  734. if minimum is not None and value < minimum:
  735. raise ValidationError(f"Passed value ({value}) is lower than minimum one")
  736. if maximum is not None and value > maximum:
  737. raise ValidationError(f"Passed value ({value}) is greater than maximum one")
  738. return value
  739. class TelegramID(Validator):
  740. def __init__(self):
  741. super().__init__(
  742. self._validate,
  743. "Telegram ID",
  744. _internal_id="TelegramID",
  745. )
  746. @staticmethod
  747. def _validate(value: ConfigAllowedTypes, /) -> int:
  748. e = ValidationError(f"Passed value ({value}) is not a valid telegram id")
  749. try:
  750. value = int(str(value).strip())
  751. except Exception:
  752. raise e
  753. if str(value).startswith("-100"):
  754. value = int(str(value)[4:])
  755. if value > 2**64 - 1 or value < 0:
  756. raise e
  757. return value
  758. class Union(Validator):
  759. def __init__(self, *validators):
  760. doc = {
  761. "en": "one of the following:\n",
  762. "ru": "одним из следующего:\n",
  763. "it": "uno dei seguenti:\n",
  764. "de": "einer der folgenden:\n",
  765. "tr": "aşağıdakilerden biri:\n",
  766. "uz": "quyidagi biri:\n",
  767. "es": "uno de los siguientes:\n",
  768. "kk": "келесілердің бірі:\n",
  769. }
  770. def case(x: str) -> str:
  771. return x[0].upper() + x[1:]
  772. for validator in validators:
  773. for key in doc:
  774. doc[key] += f"- {case(validator.doc.get(key, validator.doc['en']))}\n"
  775. for key, value in doc.items():
  776. doc[key] = value.strip()
  777. super().__init__(
  778. functools.partial(self._validate, validators=validators),
  779. doc,
  780. _internal_id="Union",
  781. )
  782. @staticmethod
  783. def _validate(
  784. value: ConfigAllowedTypes,
  785. /,
  786. *,
  787. validators: list,
  788. ) -> ConfigAllowedTypes:
  789. for validator in validators:
  790. try:
  791. return validator.validate(value)
  792. except ValidationError:
  793. pass
  794. raise ValidationError(f"Passed value ({value}) is not valid")
  795. class NoneType(Validator):
  796. def __init__(self):
  797. super().__init__(
  798. self._validate,
  799. {
  800. "en": "empty value",
  801. "ru": "пустым значением",
  802. "it": "valore vuoto",
  803. "de": "leeren Wert",
  804. "tr": "boş değer",
  805. "uz": "bo'sh qiymat",
  806. "es": "valor vacío",
  807. "kk": "бос мән",
  808. },
  809. _internal_id="NoneType",
  810. )
  811. @staticmethod
  812. def _validate(value: ConfigAllowedTypes, /) -> None:
  813. if not value:
  814. raise ValidationError(f"Passed value ({value}) is not None")
  815. return None
  816. class Hidden(Validator):
  817. def __init__(self, validator: typing.Optional[Validator] = None):
  818. if not validator:
  819. validator = String()
  820. super().__init__(
  821. functools.partial(self._validate, validator=validator),
  822. validator.doc,
  823. _internal_id="Hidden",
  824. )
  825. @staticmethod
  826. def _validate(
  827. value: ConfigAllowedTypes,
  828. /,
  829. *,
  830. validator: Validator,
  831. ) -> ConfigAllowedTypes:
  832. return validator.validate(value)
  833. class Emoji(Validator):
  834. """
  835. Checks whether passed argument is a valid emoji
  836. :param quantity: Number of emojis to be passed
  837. :param min_len: Minimum number of emojis
  838. :param max_len: Maximum number of emojis
  839. """
  840. def __init__(
  841. self,
  842. length: typing.Optional[int] = None,
  843. min_len: typing.Optional[int] = None,
  844. max_len: typing.Optional[int] = None,
  845. ):
  846. if length is not None:
  847. doc = {
  848. "en": f"{length} emojis",
  849. "ru": f"ровно {length} эмодзи",
  850. "it": f"{length} emoji",
  851. "de": f"genau {length} Emojis",
  852. "tr": f"tam {length} emoji",
  853. "uz": f"to'g'ri {length} emoji",
  854. "es": f"exactamente {length} emojis",
  855. "kk": f"тоғыз {length} емодзи",
  856. }
  857. elif min_len is not None and max_len is not None:
  858. doc = {
  859. "en": f"{min_len} to {max_len} emojis",
  860. "ru": f"от {min_len} до {max_len} эмодзи",
  861. "it": f"{min_len} a {max_len} emoji",
  862. "de": f"zwischen {min_len} und {max_len} Emojis",
  863. "tr": f"{min_len} ile {max_len} arasında emoji",
  864. "uz": f"{min_len} dan {max_len} gacha emoji",
  865. "es": f"entre {min_len} y {max_len} emojis",
  866. "kk": f"{min_len} ден {max_len} ге емодзи",
  867. }
  868. elif min_len is not None:
  869. doc = {
  870. "en": f"at least {min_len} emoji",
  871. "ru": f"не менее {min_len} эмодзи",
  872. "it": f"almeno {min_len} emoji",
  873. "de": f"mindestens {min_len} Emojis",
  874. "tr": f"en az {min_len} emoji",
  875. "uz": f"kamida {min_len} emoji",
  876. "es": f"al menos {min_len} emojis",
  877. "kk": f"кем дегенде {min_len} емодзи",
  878. }
  879. elif max_len is not None:
  880. doc = {
  881. "en": f"no more than {max_len} emojis",
  882. "ru": f"не более {max_len} эмодзи",
  883. "it": f"non più di {max_len} emoji",
  884. "de": f"maximal {max_len} Emojis",
  885. "tr": f"en fazla {max_len} emoji",
  886. "uz": f"{max_len} dan ko'proq emoji",
  887. "es": f"no más de {max_len} emojis",
  888. "kk": f"{max_len} ден асты емодзи",
  889. }
  890. else:
  891. doc = {
  892. "en": "emoji",
  893. "ru": "эмодзи",
  894. "it": "emoji",
  895. "de": "Emoji",
  896. "tr": "emoji",
  897. "uz": "emoji",
  898. "es": "emojis",
  899. "kk": "емодзи",
  900. }
  901. super().__init__(
  902. functools.partial(
  903. self._validate,
  904. length=length,
  905. min_len=min_len,
  906. max_len=max_len,
  907. ),
  908. doc,
  909. _internal_id="Emoji",
  910. )
  911. @staticmethod
  912. def _validate(
  913. value: ConfigAllowedTypes,
  914. /,
  915. *,
  916. length: typing.Optional[int],
  917. min_len: typing.Optional[int],
  918. max_len: typing.Optional[int],
  919. ) -> str:
  920. value = str(value)
  921. passed_length = len(list(grapheme.graphemes(value)))
  922. if length is not None and passed_length != length:
  923. raise ValidationError(f"Passed value ({value}) is not {length} emojis long")
  924. if (
  925. min_len is not None
  926. and max_len is not None
  927. and (passed_length < min_len or passed_length > max_len)
  928. ):
  929. raise ValidationError(
  930. f"Passed value ({value}) is not between {min_len} and {max_len} emojis"
  931. " long"
  932. )
  933. if min_len is not None and passed_length < min_len:
  934. raise ValidationError(
  935. f"Passed value ({value}) is not at least {min_len} emojis long"
  936. )
  937. if max_len is not None and passed_length > max_len:
  938. raise ValidationError(
  939. f"Passed value ({value}) is not no more than {max_len} emojis long"
  940. )
  941. if any(emoji not in ALLOWED_EMOJIS for emoji in grapheme.graphemes(value)):
  942. raise ValidationError(
  943. f"Passed value ({value}) is not a valid string with emojis"
  944. )
  945. return value
  946. class EntityLike(RegExp):
  947. def __init__(self):
  948. super().__init__(
  949. regex=r"^(?:@|https?://t\.me/)?(?:[a-zA-Z0-9_]{5,32}|[a-zA-Z0-9_]{1,32}\?[a-zA-Z0-9_]{1,32})$",
  950. description={
  951. "en": "link to entity, username or Telegram ID",
  952. "ru": "ссылка на сущность, имя пользователя или Telegram ID",
  953. "it": "link all'ent entità, nome utente o ID Telegram",
  954. "de": "Link zu einer Entität, Benutzername oder Telegram-ID",
  955. "tr": "bir varlığa bağlantı, kullanıcı adı veya Telegram kimliği",
  956. "uz": "entityga havola, foydalanuvchi nomi yoki Telegram ID",
  957. "es": "enlace a la entidad, nombre de usuario o ID de Telegram",
  958. "kk": "сынаққа сілтеме, пайдаланушы аты немесе Telegram ID",
  959. },
  960. )
  961. @staticmethod
  962. def _validate(
  963. value: ConfigAllowedTypes,
  964. /,
  965. *,
  966. regex: str,
  967. flags: typing.Optional[re.RegexFlag],
  968. ) -> typing.Union[str, int]:
  969. value = super()._validate(value, regex=regex, flags=flags)
  970. if value.isdigit():
  971. if value.startswith("-100"):
  972. value = value[4:]
  973. value = int(value)
  974. if value.startswith("https://t.me/"):
  975. value = value.split("https://t.me/")[1]
  976. if not value.startswith("@"):
  977. value = f"@{value}"
  978. return value