How to use delete_comment method in Kiwi

Best Python code snippet using Kiwi_python

twsp.py

Source:twsp.py Github

copy

Full Screen

1#!/usr/bin/env python32# (C) 2021 gomachssm3import logging4import os5import pathlib6from copy import deepcopy7from uuid import uuid48from functools import lru_cache9from .internal_exceptions import Msg, TwspException, TwspExecuteError, TwspValidateError10from .enums import ParamStyle, CommentType11logger = logging.getLogger(__name__)12NEWLINE_CHAR = '\n'13_TN = 'tmpnm'14_VL = 'value'15_DMY = '...dummy...'16_END = '/*end*/'17_FIND_TARGETS = ('"', "'", '(', '[', '{', '--', '/*', )18_BRACKET_PARE = {'[': ']', '(': ')', '{': '}'}19# ([{'"\ 以外の一般的な記号と空白文字20_EOP_SIMPLE_PARAM = ('!', '#', '$', '%', '&', ')', '-', '=', '^', '~',21 '|', '@', '`', ';', '+', ':', '*', ']', '}',22 ',', '/', '<', '>', '?', ' ', # '.', '(', '_',23 '\t', '\n', '\r', )24_EOP_END = (_END, )25def parse_file(file_path: str, query_params=None, delete_comment=True, encoding='utf-8', newline='\n',26 paramstyle: ParamStyle = None) -> (str, dict):27 """SQLファイルを読み込み、解析を行う28 Args:29 file_path (str): 実行対象SQLのファイルパス(必須)30 query_params (dict): SQL実行時に利用するパラメータ31 delete_comment (bool): True の場合、通常コメントを削除、 False の場合は削除しない32 デフォルトは True33 encoding (str): 対象ファイルの文字コード デフォルトは utf-834 newline (str): 対象ファイルの改行コード (デフォルトは '\n')35 paramstyle (ParamStyle): 解析後のSQLパラメータ書式を決める。未指定時は `NAMED` と同じ扱い36 Returns:37 tuple(str, dict):38 str: 解析後のSQL39 dict: パラメータ更新後のdict40 """41 try:42 base_sql = _open_file(file_path, encoding=encoding)43 sql, qparams = parse_sql(base_sql, query_params, delete_comment=delete_comment, newline=newline,44 paramstyle=paramstyle)45 return sql, qparams46 except TwspException as e:47 logger.error(e.msg_txt)48def parse_sql(base_sql: str, query_params=None, delete_comment=True, newline='\n',49 paramstyle: ParamStyle = None) -> (str, dict):50 """SQLの解析を行う.51 Args :52 base_sql (str): 解析対象SQL(必須)53 query_params (dict): SQL実行時に利用するパラメータ54 delete_comment (bool): True の場合、通常コメントを削除、 False の場合は削除しない (デフォルトは True)55 newline (str): SQLに含まれる改行コード (デフォルトは '\n')56 paramstyle (ParamStyle): 解析後のSQLパラメータ書式を決める。未指定時は `NAMED` と同じ扱い57 Returns:58 tuple(str, dict):59 str: 解析後のSQL60 dict: パラメータ更新後のdict61 """62 global NEWLINE_CHAR63 NEWLINE_CHAR = newline64 pstyle = paramstyle or ParamStyle.NAMED65 _validate_paramstyle(pstyle)66 # PARAMSTYLEの値に応じたパラメータ書式のセット67 # TODO: グローバルではなく、引数で持ち回せるようにしたい68 prmfmt = pstyle.value69 try:70 qparams = deepcopy(query_params) if query_params else {}71 _is_collect_type('base_sql', base_sql, str)72 _is_collect_type('query_params', qparams, dict)73 # base_sql = _format_line_forifend(base_sql)74 sql, _, _ = _parse(base_sql, qparams, delete_comment, prmfmt)75 return sql, qparams76 except TwspException as e:77 logger.error(e)78 # logger.error(e.msg_txt)79def _parse(base_sql: str, qparams: dict, delete_comment: bool, prmfmt: str, eop=None, tmp_params=None,80 after_pcmt=False, ldng_sp_cnt=0) -> (str, int, int):81 """ SQLを解析してパラメータを埋め込む82 Args:83 base_sql (str): 元となるSQL84 qparams (dict): パラメータ85 delete_comment (bool): コメント削除フラグ86 eop (tuple->str or None): 解析終了文字 End of parse87 tmp_params (dict): for内の一時パラメータ default: None88 after_pcmt (bool): パラメータ直後フラグ default: False89 ldng_sp_cnt (int): 行頭から続く空白文字の個数 default: 090 Returns:91 tuple:92 str: 構築したSQL93 int: 最後の改行から連続したスペースの個数94 1以上: その個数分のスペースが末尾に存在する95 0以下: 末尾にスペースが存在しない96 int: return時のインデックス97 """98 q = []99 max_idx = len(base_sql)100 i = 0101 tmp_params = tmp_params if tmp_params else {}102 while i < max_idx and not _startswith_eop(base_sql, i, eop):103 c, cc, i = _next_chars(base_sql, i, max_idx, eop)104 c, i, del_last_sp = _parse_switch_by_char(base_sql, c, cc, i, qparams, delete_comment, tmp_params, after_pcmt,105 ldng_sp_cnt, prmfmt)106 if del_last_sp:107 q = _delete_last_space(q)108 ldng_sp_cnt = _update_blank_line(ldng_sp_cnt, c)109 q.append(c)110 result = ''.join(q)111 return result, ldng_sp_cnt, i112def _parse_switch_by_char(base_sql: str, c: str, cc: str, i: int, qparams: dict, delete_comment: bool,113 tmp_params: dict, after_pcmt: bool, ldng_sp_cnt: int, prmfmt: str) -> (str, int, bool):114 del_last_sp = False115 if c in ("'", '"', '(', '[', '{'):116 c, i = _nextstring(c, base_sql, i, qparams, delete_comment, after_pcmt)117 elif cc == '--':118 # 行コメント -- の場合119 c, addi = _get_single_line_comment(base_sql[i - 1:], delete_comment)120 i += addi - 1121 elif cc == '/*':122 # 複数行コメント /*...*/ の場合123 c, addi, del_last_sp = _get_multi_line_comment(base_sql[i - 1:], qparams, delete_comment, tmp_params,124 ldng_sp_cnt, prmfmt)125 i += addi - 1126 return c, i, del_last_sp127def _nxtchr(string: str, idx: int):128 return string[idx], idx + 1129def _fnd(string: str, target: str):130 index = string.find(target)131 return index if 0 <= index else len(string)132def _next_chars(string: str, idx: int, mxidx: int, eop=None) -> (str, str, int):133 targets = _FIND_TARGETS + eop if eop else _FIND_TARGETS134 min_idx = min([_fnd(string[idx:], tgt) for tgt in targets])135 if min_idx == 0:136 c, i = _nxtchr(string, idx)137 cc = f'{c}{string[i]}' if i < mxidx else None138 else:139 i = idx + min_idx140 c = string[idx:i]141 cc = c142 return c, cc, i143def _nextstring(c: str, base_sql: str, i: int, qparams: dict, delete_comment: bool, after_pcmt: bool) -> (str, int):144 addi = 0145 if c in ("'", '"'):146 # ' か " の文字列の場合147 c, addi = _nextquote(c, base_sql[i:])148 elif after_pcmt and c in ('(', '[', '{'):149 # パラメータ直後が括弧の場合150 c, addi = _nextbracket(c, base_sql[i:], qparams, delete_comment)151 return c, i + addi152def _nextquote(quote: str, sql_after_quote: str) -> (str, int):153 chars = [quote]154 max_idx = len(sql_after_quote) - 1155 addi = 0156 while addi <= max_idx:157 c, addi = _nxtchr(sql_after_quote, addi)158 if c == quote:159 # 2連続のクォート以外の場合、文字列はそこまで160 if max_idx <= addi or sql_after_quote[addi + 1] != quote:161 chars.append(c)162 break163 chars.append(c)164 c, addi = _nxtchr(sql_after_quote, addi)165 chars.append(c)166 quote_string = ''.join(chars)167 return quote_string, addi168def _nextbracket(openbrkt: str, sql_after_brkt: str, qparams: dict, delete_comment: bool) -> (str, int):169 closingbrkt = _BRACKET_PARE.get(openbrkt)170 chars = [openbrkt]171 max_idx = len(sql_after_brkt) - 1172 addi = 0173 while addi <= max_idx:174 c, addi = _nxtchr(sql_after_brkt, addi)175 chars.append(c)176 if c == closingbrkt:177 break178 bracket_strings = ''.join(chars)179 return bracket_strings, addi180def _get_single_line_comment(sql_comment: str, delete_comment: bool) -> (str, int):181 c, addi = _single_line_comment(sql_comment)182 if delete_comment:183 c = ''184 return c, addi185def _single_line_comment(sql_comment: str) -> (str, int):186 # この関数が呼び出される時、文字列は必ず -- から始まる187 maxlen = len(sql_comment)188 new_line_idx = sql_comment.find(NEWLINE_CHAR)189 new_line_idx = maxlen if new_line_idx < 0 else new_line_idx190 comment_string = sql_comment[:new_line_idx]191 return comment_string, new_line_idx192def _get_multi_line_comment(sql_comment: str, qparams: dict, delete_comment: bool, tmp_params: dict,193 ldng_sp_cnt: int, prmfmt: str) -> (str, int, bool):194 c, addi, is_comment, delete_last_sp = _multi_line_comment(sql_comment, qparams, delete_comment, tmp_params,195 ldng_sp_cnt, prmfmt)196 if is_comment and delete_comment:197 c = ''198 return c, addi, delete_last_sp199def _multi_line_comment(sql_comment: str, qparams: dict, delete_comment: bool, tmp_params: dict,200 ldng_sp_cnt: int, prmfmt: str) -> (str, int, bool):201 # この関数が呼び出される時、文字列は必ず /* から始まる202 cmtype, comment_string = _check_comment_type(sql_comment)203 is_comment = False204 del_last_sp = False205 # ex: comment_string : "/*:parameter*/"206 # after_comment : "'hogehoge' from xxx"207 after_comment = sql_comment[len(comment_string):]208 if cmtype is CommentType.PARAM: # /*:param*/209 comment_string, idx = _parse_param_comment(comment_string, after_comment, qparams, tmp_params, prmfmt)210 elif cmtype is CommentType.DIRECT: # /*$param*/211 comment_string, idx = _parse_direct_comment(comment_string, after_comment, qparams, tmp_params, prmfmt)212 elif cmtype is CommentType.IFEND: # /*%if ~*/213 # TODO: ELSE対応214 comment_string, idx, del_last_sp = _parse_if_comment(215 comment_string, after_comment, qparams, delete_comment, tmp_params, ldng_sp_cnt, prmfmt)216 elif cmtype is CommentType.FOREND: # /*%for ~*/217 comment_string, idx, del_last_sp = _parse_for_comment(218 comment_string, after_comment, qparams, delete_comment, tmp_params, ldng_sp_cnt, prmfmt)219 elif cmtype is CommentType.NORMAL:220 is_comment = True221 idx = len(comment_string)222 else:223 raise TwspException(Msg.E0003, sql_comment)224 return comment_string, idx, is_comment, del_last_sp225def _parse_param_comment(comment_string: str, after_comment: str, qparams: dict, tmp_params: dict,226 prmfmt: str) -> (str, int):227 """228 Args:229 comment_string (str): コメント部分の文字列230 ex: /*:any_parameter*/231 after_comment (str): コメント直後の文字列232 Returns:233 tuple:234 str:235 int:236 """237 bind_name, _, load_len = _parse_simple_comment(comment_string, after_comment, qparams, tmp_params, prmfmt)238 return bind_name, load_len239def _parse_direct_comment(comment_string: str, after_comment: str, qparams: dict, tmp_params: dict,240 prmfmt: str) -> (str, int):241 """242 Args:243 comment_string (str): コメント部分の文字列244 ex: /*$any_parameter*/245 after_comment (str): コメント直後の文字列246 qparams (dict): SQL実行時に利用するパラメータ247 Returns:248 tuple:249 str:250 int:251 """252 merged_params = _merge_qparams(qparams, tmp_params)253 _, value, load_len = _parse_simple_comment(comment_string, after_comment, merged_params, {}, prmfmt)254 return value, load_len255def _parse_simple_comment(comment_string: str, after_comment: str, qparams: dict, tmp_params: dict,256 prmfmt: str) -> (str, str, int):257 # comment_string: '/*:param*/' or '/*$param*/'258 param_name = comment_string[3:-2]259 bind_name, value = _update_qparams_if_exist_tmp(param_name, qparams, tmp_params, prmfmt)260 dummy_value, _, _ = _parse(after_comment, {}, False, prmfmt, eop=_EOP_SIMPLE_PARAM, after_pcmt=True)261 load_len = len(comment_string) + len(dummy_value)262 return bind_name, value, load_len263def _update_qparams_if_exist_tmp(param: str, qparams: dict, tmp_params: dict, prmfmt: str):264 tmp_name = tmp_params.get(param, {}).get(_TN)265 if _tmpp_is_dummy(tmp_params):266 # tmp_paramsがダミーの場合267 updated_bind_name = prmfmt.format(param)268 updated_param_value = ''269 elif tmp_name is not None:270 updated_bind_name = prmfmt.format(tmp_name)271 updated_param_value = tmp_params.get(param, {}).get(_VL)272 qparams[tmp_name] = updated_param_value273 else:274 updated_bind_name = prmfmt.format(param)275 updated_param_value = qparams.get(param)276 return updated_bind_name, f'{updated_param_value}'277def _parse_if_comment(comment_string: str, after_comment: str, qparams: dict, delete_comment: bool,278 tmp_params: dict, ldng_sp_cnt: int, prmfmt: str) -> (str, int, bool):279 if_strings, after_idx, del_last_sp = _get_str_in_forif(after_comment, qparams, delete_comment, tmp_params,280 ldng_sp_cnt, prmfmt)281 is_true = _execute_if_statement(comment_string[3:-2], qparams, tmp_params)282 output_value = ''.join(if_strings) if is_true else ''283 addi = len(comment_string) + after_idx284 return output_value, addi, del_last_sp285def _parse_for_comment(comment_string: str, after_comment: str, qparams: dict, delete_comment: bool,286 tmp_params: dict, ldng_sp_cnt: int, prmfmt: str) -> (str, int, bool):287 # comment_string: /*%for x in list_obj*/288 # after_comment: .*/*end*/289 for_strings, after_idx, del_last_sp = [], 0, False290 # 最後の1回はfor~endまでの文字数カウントのため、ダミーで空dictが返される291 for tmp_params in _get_for_variable_names(comment_string, qparams, tmp_params):292 _for_strings, after_idx, del_last_sp = _get_str_in_forif(after_comment, qparams, delete_comment, tmp_params,293 ldng_sp_cnt, prmfmt)294 for_strings.append(_for_strings)295 # 最後の1ループ分は無視296 output_value = ''.join(for_strings[:-1])297 addi = len(comment_string) + after_idx298 return output_value, addi, del_last_sp299def _get_for_variable_names(for_comment: str, qparams: dict, tmp_params: dict) -> dict:300 """301 Args:302 for_comment (str): '/*%for x in xxx*/'303 qparams (dict):304 tmp_params (dict):305 ex1: {}306 ex2:307 ex3: {__DMY: True}308 Returns:309 """310 vnames = [v.strip() for v in for_comment[7:-2].split(' in ')[0].split(',')]311 merged_params = _merge_qparams(qparams, tmp_params)312 vnames_str = ",".join(vnames)313 for_statement = f'for {vnames_str} in []' if _tmpp_is_dummy(tmp_params) else for_comment[3:-2]314 try:315 values_list = eval(f'[({vnames_str}) {for_statement}]', {}, merged_params)316 except Exception as e:317 raise TwspExecuteError(Msg.E0008, e, for_statement)318 prefix = str(uuid4()).replace('-', '_')319 for tmp_variable in _enum_temp_variables(vnames, values_list, prefix):320 yield {**tmp_params, **tmp_variable}321 yield {_DMY: True}322 # 変数名のリスト323 # for a in range(3)なら324 # -> {'a': {'tmpnm': 'xxxxxxxxxxxx_0_a', 'value': 0}}325 # -> {'a': {'tmpnm': 'xxxxxxxxxxxx_1_a', 'value': 1}}326 # -> {'a': {'tmpnm': 'xxxxxxxxxxxx_2_a', 'value': 2}}327 # -> {__DMY: True}328 # for a,b,c in zip(['A', 'b'], ['I', 'j'], ['X', 'y'])なら、329 # -> {'a': {'tmpnm': 'xxxxxxxxxxxx_0_a', 'value': 'A'},330 # 'b': {'tmpnm': 'xxxxxxxxxxxx_0_b', 'value': 'I'},331 # 'c': {'tmpnm': 'xxxxxxxxxxxx_0_c', 'value': 'X'}}332 # -> {'a': {'tmpnm': 'xxxxxxxxxxxx_1_a', 'value': 'b'},333 # 'b': {'tmpnm': 'xxxxxxxxxxxx_1_b', 'value': 'j'},334 # 'c': {'tmpnm': 'xxxxxxxxxxxx_1_c', 'value': 'y'}}335 # -> {__DMY: True}336def _enum_temp_variables(vnames: list, values_list: list, prefix: str):337 if len(vnames) == 1:338 for vi, values in enumerate(values_list):339 vname = vnames[0]340 tmp_name = _gen_tmp_name(vname, vi, prefix)341 yield {vname: {_TN: tmp_name, _VL: values}}342 else:343 for vi, values in enumerate(values_list):344 tmpdct = {}345 for ni, vname in enumerate(vnames):346 tmp_name = _gen_tmp_name(vname, vi, prefix)347 tmpdct[vname] = {_TN: tmp_name, _VL: values[ni]}348 yield tmpdct349def _gen_tmp_name(vname: str, loop_count: int, prefix: str) -> str:350 name = f'{prefix}_{loop_count}_{vname}'351 return name352def _get_str_in_forif(after_comment: str, qparams: dict, delete_comment: bool, tmp_params: dict,353 ldng_sp_cnt: int, prmfmt: str) -> (str, int, bool):354 """355 Args:356 after_comment (str): /*%for ~*/または/*%if ~*/の直後に続く文字列357 qparams (dict): パラメータ358 delete_comment (bool):359 tmp_params (dict):360 ldng_sp_cnt (int):361 Returns:362 """363 # for ~ end、や if ~ end に挟まれた文字を取得する364 nextq, after_idx, del_last_sp = _nxtq_in_forif(after_comment, ldng_sp_cnt)365 # 改行削除済みの場合、 ldng_sp_cnt は 0 から366 ldng_sp_cnt = -1 if after_comment == nextq else 0367 output_value, _, vlen = _parse(nextq, qparams, delete_comment, prmfmt, _EOP_END, tmp_params=tmp_params,368 ldng_sp_cnt=ldng_sp_cnt)369 nextq = nextq[vlen:]370 after_idx += vlen371 output_value, addi = _end_in_forif(output_value, nextq)372 after_idx += addi373 return output_value, after_idx, del_last_sp374def _nxtq_in_forif(nextq: str, ldng_sp_cnt: int) -> (str, int, bool):375 if 0 <= ldng_sp_cnt:376 nlidx = nextq.find(NEWLINE_CHAR) + 1377 if nextq[:nlidx].strip() == '':378 return nextq[nlidx:], nlidx, True379 return nextq, 0, False380def _end_in_forif(output_value: str, nextq: str) -> (str, int):381 # /*end*/が見つからずSQLの末尾に到達した場合382 if not nextq:383 return output_value, 0384 # nextq: '/*end*/\n order by ~'385 # /*end*/ でsplitするのは1回のみ386 target_last_line = output_value.split(NEWLINE_CHAR)[-1]387 next_first_line = nextq.split(NEWLINE_CHAR, 1)[0]388 sp_stripped = f'{target_last_line}{next_first_line}'.strip(' ')389 if sp_stripped == _END:390 # /*end*/の行に含まれる文字がスペースのみの場合、次の行から読み込ませる391 newline_len = len(NEWLINE_CHAR)392 output_idx = len(output_value) - len(target_last_line)393 forif_string = output_value[:output_idx]394 add_idx = len(next_first_line) + newline_len395 return forif_string, add_idx396 else:397 # /*end*/までの文字数を返す len('/*end*/')398 return output_value, 7399def _is_continue(string: str, end_str: str):400 if not string or string.startswith(end_str):401 return False402 return True403def _execute_if_statement(statement: str, qparams: dict, tmp_params: dict) -> bool:404 # statement: "if BOOL_EXPRESSION"405 if _tmpp_is_dummy(tmp_params):406 return True407 try:408 tparams = {}409 for key, value in _tmpp_items(tmp_params):410 tparams[key] = value411 is_true = eval(f'True {statement} else False', {}, {**qparams, **tparams})412 except NameError as e:413 raise TwspExecuteError(Msg.E0005, e, statement)414 if type(is_true) != bool:415 raise TwspExecuteError(Msg.E0006, statement)416 return is_true417def _check_comment_type(sql_after_comment: str) -> (CommentType, str):418 # /*:param*/ -> :param 利用できる文字は英数字とアンダースコア419 # /*$param*/ -> {param} 利用できる文字は英数字とアンダースコア420 # /*%if a == b*/hoge/*end*/ -> 条件がTrueの場合、/*end*/までに挟まれた部分が出力421 # /*%for a in b*/and a /*end*/ -> and a and a and a and a ... iterate by b422 # other patterns are multi line comment.423 for ctyp in CommentType:424 matched = ctyp.value.findall(sql_after_comment)425 if len(matched) > 0:426 return ctyp, matched[0]427def __get_cache_maxsize() -> int:428 default_size = 20429 env_size = os.getenv('TWSP_CACHE_SIZE')430 try:431 size = int(env_size)432 except Exception:433 size = None434 return size if size else default_size435@lru_cache(maxsize=__get_cache_maxsize())436def _open_file(file_path, encoding='utf-8'):437 _is_collect_type('file_path', file_path, str)438 if not _is_absolute(file_path):439 raise TwspValidateError(Msg.E0002, file_path)440 with open(file_path, 'r', encoding=encoding) as f:441 return f.read()442def _is_absolute(path):443 p = pathlib.Path(path)444 return p.is_absolute()445def _is_collect_type(argname, value, expected_type):446 if type(value) != expected_type:447 raise TwspValidateError(Msg.E0001, argname, expected_type, type(value))448def _startswith_eop(base_sql: str, i: int, eop: list) -> bool:449 if not eop:450 return False451 for e in eop:452 if base_sql[i:].startswith(f'{e}'):453 return True454 return False455def _update_blank_line(ldng_sp_cnt: int, c: str) -> int:456 if not c:457 return ldng_sp_cnt458 ridx = c.rfind(NEWLINE_CHAR)459 if ldng_sp_cnt < 0 and ridx < 0 == 1:460 return -1461 after_nl = c[ridx + 1:]462 if after_nl.lstrip(' ') == '':463 # 改行後、全部スペースの場合464 return len(after_nl)465 else:466 return -1467def _delete_last_space(q: list) -> list:468 idx = len(q) - 1469 while -1 < idx:470 idx_str = q[idx].rstrip(' ')471 if idx_str:472 q[idx] = idx_str473 break474 del q[idx]475 idx -= 1476 return q477# #######################################478# temp_params479# #######################################480def _tmpp_is_dummy(tmpp: dict) -> bool:481 return True if tmpp.get(_DMY) is True else False482def _tmpp_items(tmpp: dict) -> (any, any):483 for key, v in tmpp.items():484 value = v.get(_VL)485 yield key, value486def _merge_qparams(qparams: dict, tmp_params: dict) -> dict:487 if _tmpp_is_dummy(tmp_params):488 return {**qparams}489 merged_params = {**qparams}490 for key, value in _tmpp_items(tmp_params):491 merged_params[key] = value492 return merged_params493def _validate_paramstyle(paramstyle):494 if (not isinstance(paramstyle, ParamStyle)) or paramstyle not in ParamStyle:495 ps = [f'{p.__class__.__name__}.{p.name}' for p in ParamStyle]496 raise TwspValidateError(Msg.E0009, f'{", ".join(ps[:-1])}', ps[-1], paramstyle)...

Full Screen

Full Screen

urls.py

Source:urls.py Github

copy

Full Screen

1"""2 @author:Mindfire3 @dateCreated:4 url container file which contaiins all the url variables of blog apps.5"""6from django.conf.urls.defaults import *7from blog.models import *8from django.core.urlresolvers import reverse9from blog import views10urlpatterns = patterns('',11 url(r"^(\d+)/$", views.post, name='post'),12 url(r"^add_comment/(\d+)/$", views.add_comment, name='add_comment'),13 url(r"^delete_comment/(\d+)/$", views.delete_comment, name='delete_comment'),14 url(r"^delete_comment/(\d+)/(\d+)/$", views.delete_comment, name='delete_comment'),15 url(r"^month/(\d+)/(\d+)/$", "month"),16 url(r'^$', views.main, name='main'),...

Full Screen

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run Kiwi automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful