{"id":473050,"date":"2025-09-02T15:18:15","date_gmt":"2025-09-02T15:18:15","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=473050"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=473050","title":{"rendered":"<span>\u041a\u0430\u043a \u041d\u0415 \u043d\u0443\u0436\u043d\u043e \u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u044b \u043d\u0430 Python<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435<\/h2>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0440\u0430\u0437\u0431\u0435\u0440\u0443 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0442\u0438\u043f\u0438\u0447\u043d\u044b\u0445 \u043e\u0448\u0438\u0431\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u044e\u0442\u0441\u044f \u043f\u0440\u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432 \u043d\u0430 Python. \u0426\u0435\u043b\u044c \u043d\u0435 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u0441\u043c\u0435\u044f\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043b\u044e\u0434\u0435\u0439 \u0438\u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u044b. \u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u2014 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0430\u0431\u0441\u0443\u0440\u0434\u043d\u043e\u0441\u0442\u044c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043f\u043e\u0434\u0445\u043e\u0434\u043e\u0432, \u043e\u0431\u044a\u044f\u0441\u043d\u0438\u0442\u044c, \u043a\u0430\u043a \u043d\u0435 \u0441\u0442\u043e\u0438\u0442 \u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u0443\u044e \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 \u044d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c.<\/p>\n<p>\u0417\u0430\u0434\u0430\u0447\u0430 \u043f\u0440\u043e\u0441\u0442\u0430\u044f: \u0441\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u0442\u044c \u0432\u0430\u043c \u0432\u0440\u0435\u043c\u044f \u0438 \u0441\u0438\u043b\u044b. \u0427\u0442\u043e\u0431\u044b \u043d\u0435 \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u043f\u043e\u0442\u043e\u043c \u00ab\u043f\u0435\u0440\u0435\u0443\u0447\u0438\u0432\u0430\u0442\u044c\u0441\u044f\u00bb, \u0438\u0437\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043e\u0442 \u043a\u043e\u0441\u0442\u044b\u043b\u0435\u0439 \u0438 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442\u044c \u0431\u043e\u043b\u0435\u0437\u043d\u0435\u043d\u043d\u044b\u0439 \u0434\u0435\u0442\u043e\u043a\u0441 \u043e\u0442 \u0441\u0430\u043c\u043e\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u00ab\u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434\u043e\u0432\u00bb. \u0413\u043e\u0440\u0430\u0437\u0434\u043e \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u0435\u0435 \u0441 \u0441\u0430\u043c\u043e\u0433\u043e \u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b \u0442\u0430\u043a, \u0447\u0442\u043e\u0431\u044b \u043a\u043e\u0434 \u0431\u044b\u043b \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c, \u043f\u043e\u043d\u044f\u0442\u043d\u044b\u043c \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u043c.<\/p>\n<blockquote>\n<p><strong>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440.<\/strong> \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u043e\u0431\u043e\u0431\u0449\u0435\u043d\u044b \u0438 \u0441\u0438\u043d\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u044b; \u0446\u0435\u043b\u044c \u2014 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u0430 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u043e\u0432. \u041b\u044e\u0431\u044b\u0435 \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u044f \u0441 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u043c\u0438 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b. \u0412\u0441\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438 \u2014 \u043f\u0440\u043e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u0438 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438, \u0430 \u043d\u0435 \u043f\u0440\u043e \u043b\u044e\u0434\u0435\u0439.<\/p>\n<\/blockquote>\n<h2>\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u043a\u0438<\/h2>\n<p>\u042d\u0442\u0430 \u0441\u0442\u0430\u0442\u044c\u044f \u043f\u043e\u044f\u0432\u0438\u043b\u0430\u0441\u044c \u043d\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e. \u041d\u0435\u0434\u0430\u0432\u043d\u043e \u043a\u043e \u043c\u043d\u0435 \u043f\u0440\u0438\u0448\u0451\u043b \u0441\u0442\u0443\u0434\u0435\u043d\u0442 \u0441 \u043a\u0443\u0440\u0441\u0430 \u0438 \u0437\u0430\u0434\u0430\u043b \u0432\u043e\u043f\u0440\u043e\u0441: <em>\u00ab\u042f \u043d\u0430\u0448\u0451\u043b \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432. \u042d\u0442\u043e \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0430? \u0422\u0430\u043a \u0434\u0435\u043b\u0430\u044e\u0442?\u00bb<\/em><\/p>\n<p>\u041a\u043e\u0433\u0434\u0430 \u044f \u043e\u0442\u043a\u0440\u044b\u043b \u0441\u0441\u044b\u043b\u043a\u0443 \u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u043b \u043a\u043e\u0434, \u0443\u0432\u0438\u0434\u0435\u043b \u043c\u043e\u043d\u043e\u043b\u0438\u0442\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441 \u043f\u0435\u0440\u0435\u043c\u0435\u0448\u0430\u043d\u043d\u044b\u043c\u0438 \u0437\u043e\u043d\u0430\u043c\u0438 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u041f\u0435\u0440\u0435\u0434\u043e \u043c\u043d\u043e\u0439 \u043e\u043a\u0430\u0437\u0430\u043b\u0430\u0441\u044c \u00ab\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u00bb, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u0437\u0438\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0441\u0435\u0431\u044f \u043a\u0430\u043a \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432 \u00ab\u043d\u0430 \u0432\u0441\u0435 \u0441\u043b\u0443\u0447\u0430\u0438 \u0436\u0438\u0437\u043d\u0438\u00bb. \u0412\u043d\u0443\u0442\u0440\u0438 \u2014 \u043e\u0434\u0438\u043d-\u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0430 3500 \u0441\u0442\u0440\u043e\u043a, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u044b\u043b\u043e \u0437\u0430\u043f\u0438\u0445\u043d\u0443\u0442\u043e \u0432\u0441\u0451 \u043f\u043e\u0434\u0440\u044f\u0434: UI-\u0442\u0435\u0441\u0442\u044b, API-\u0442\u0435\u0441\u0442\u044b, \u043e\u0431\u0451\u0440\u0442\u043a\u0438, \u0442\u0443\u043b\u0437\u044b, \u0445\u0435\u043b\u043f\u0435\u0440\u044b, \u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u0438 \u0434\u0430\u0436\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0435 \u0443\u0442\u0438\u043b\u0438\u0442\u044b. \u041f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f \u043d\u0435 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a, \u0430 \u043c\u043e\u043d\u043e\u043b\u0438\u0442 \u0431\u0435\u0437 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b.<\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/945\/7a6\/de7\/9457a6de7e6ae69a45dee271cec876f6.png\" width=\"750\" height=\"499\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/945\/7a6\/de7\/9457a6de7e6ae69a45dee271cec876f6.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/945\/7a6\/de7\/9457a6de7e6ae69a45dee271cec876f6.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>\u0418 \u0441\u0430\u043c\u043e\u0435 \u0443\u0434\u0438\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435: \u0441\u043e \u0441\u043b\u043e\u0432 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430, \u044d\u0442\u043e\u0442 \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u00bb \u043f\u0440\u0435\u043f\u043e\u0434\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u043a\u0430\u043a \u00ab\u043b\u0451\u0433\u043a\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u044b\u00bb. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043c, \u043f\u043e\u0447\u0435\u043c\u0443 \u044d\u0442\u043e \u0441\u043e\u0432\u0441\u0435\u043c \u043d\u0435 \u043b\u0451\u0433\u043a\u0438\u0439 \u043f\u0443\u0442\u044c, \u0430 \u0441\u043a\u043e\u0440\u0435\u0435 \u0431\u044b\u0441\u0442\u0440\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c \u0442\u0435\u0441\u0442\u0430\u043c \u0438 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u043e\u043c\u0443 \u0434\u043e\u043b\u0433\u0443.<\/p>\n<blockquote>\n<p><strong>\u0421\u043a\u0430\u0436\u0443 \u0441\u0440\u0430\u0437\u0443:<\/strong> \u044f \u043d\u0435 \u0431\u0443\u0434\u0443 \u0434\u0430\u0432\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043e\u043a \u0438 \u043d\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u043e\u0432. \u0426\u0435\u043b\u044c \u0441\u0442\u0430\u0442\u044c\u0438 \u043d\u0435 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u043a\u043e\u0433\u043e-\u0442\u043e \u0432\u044b\u0441\u043c\u0435\u0438\u0432\u0430\u0442\u044c \u0438\u043b\u0438 \u0443\u043d\u0438\u0437\u0438\u0442\u044c. \u0426\u0435\u043b\u044c \u2014 \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438, \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u0438\u0442\u044c \u043a\u043e\u0441\u0442\u044b\u043b\u0438, \u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434\u044b \u0438 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b. \u041f\u043e\u0434\u043e\u0431\u043d\u044b\u0439 \u043a\u043e\u0434, \u0443\u0432\u044b, \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0434\u0435\u0441\u044c: \u043e\u043d \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u0445, \u0434\u0430 \u0435\u0449\u0451 \u0438 \u043f\u043e\u0434\u0430\u0451\u0442\u0441\u044f \u043d\u043e\u0432\u0438\u0447\u043a\u0430\u043c \u043a\u0430\u043a \u00ab\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434\u00bb.<\/p>\n<\/blockquote>\n<p>\u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0432\u043c\u0435\u0441\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0434\u0451\u043c \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u00ab\u0434\u0435\u0442\u043e\u043a\u0441\u00bb \u043e\u0442 \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u0439.<\/p>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 1. \u00ab\u0422\u0430\u043d\u0446\u044b \u0441\u043e \u0441\u0442\u0440\u0435\u043b\u043e\u0447\u043a\u0430\u043c\u0438 \u0432\u043d\u0438\u0437\u00bb<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412 \u043a\u043e\u0434\u0435 \u0434\u0435\u0441\u044f\u0442\u043a\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0432\u0438\u0434\u0430 \u00ab\u043d\u0430\u0436\u043c\u0438 \u0441\u0442\u0440\u0435\u043b\u043a\u0443 \u0432\u043d\u0438\u0437 N \u0440\u0430\u0437, \u0432\u0434\u0440\u0443\u0433 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u043e\u043a\u0430\u0436\u0435\u0442\u0441\u044f \u0432 \u0432\u0438\u0434\u0438\u043c\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438\u00bb. \u0427\u0430\u0441\u0442\u043e \u0435\u0449\u0451 \u0441 <code>time.sleep(0.1)<\/code> \u0432 \u0446\u0438\u043a\u043b\u0435 \u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u043e\u0439 \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c \u00ab\u043a\u043e\u0433\u0434\u0430 \u043f\u043e\u0432\u0435\u0437\u0451\u0442\u00bb.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">import time from selenium.webdriver import ActionChains from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC   def make_displayed_with_arrow_down_and_click(driver, xpath, waiting_time):     end = time.time() + waiting_time     while time.time() &lt; end:         try:             el = WebDriverWait(driver, 0.2).until(                 EC.visibility_of_element_located((By.XPATH, xpath))             )             if el.is_displayed():                 el.click()                 return True         except:             pass         ActionChains(driver).send_keys(Keys.ARROW_DOWN).perform()         time.sleep(0.1)     return False <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>Flaky \u0438 \u0433\u043e\u043d\u043a\u0438. <code>time.sleep()<\/code> \u043c\u0430\u0441\u043a\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438, \u0430 \u043d\u0435 \u0440\u0435\u0448\u0430\u0435\u0442 \u0435\u0451. \u041d\u0430 CI \u0442\u0430\u043a\u0438\u0435 \u0442\u0435\u0441\u0442\u044b \u00ab\u043c\u0438\u0433\u0430\u044e\u0442\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u043e\u0442 \u0444\u043e\u043a\u0443\u0441\u0430. \u041a\u043b\u0430\u0432\u0438\u0448\u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u0432 \u0444\u043e\u043a\u0443\u0441\u0435. \u041b\u044e\u0431\u043e\u0439 \u043f\u043e\u043f-\u0430\u043f\/\u043c\u043e\u0434\u0430\u043b \u2014 \u0438 \u0432\u0441\u0451 \u0441\u043b\u043e\u043c\u0430\u043b\u043e\u0441\u044c.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\/\u0440\u0430\u0437\u0434\u0443\u0432\u0430\u043d\u0438\u0435. \u0412\u0430\u0440\u0438\u0430\u0446\u0438\u0438 \u00ab\u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u0432\u043d\u0438\u0437\/\u0432\u0432\u0435\u0440\u0445\/ENTER\/SPACE\u00bb \u043f\u043b\u043e\u0434\u044f\u0442 \u0434\u0435\u0441\u044f\u0442\u043a\u0438 \u043e\u0434\u043d\u043e\u0442\u0438\u043f\u043d\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u0445\u043e\u0434 DOM-\u043c\u043e\u0434\u0435\u043b\u0438. \u0412\u043c\u0435\u0441\u0442\u043e \u044f\u0432\u043d\u043e\u0433\u043e \u0441\u043a\u0440\u043e\u043b\u043b\u0430 \u043a \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 \u2014 \u00ab\u043d\u0430\u0434\u0435\u0435\u043c\u0441\u044f\u00bb, \u0447\u0442\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0441\u0430\u043c\u0430 \u043f\u0440\u043e\u043c\u043e\u0442\u0430\u0435\u0442\u0441\u044f.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0435\u0448\u0435\u043d\u0438\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0439. \u041f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u044f\u0432\u043d\u044b\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u2014 \u0438\u0442\u043e\u0433\u043e\u043c \u0441\u0442\u0430\u043d\u043e\u0432\u044f\u0442\u0441\u044f \u043d\u0435\u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0443\u0435\u043c\u044b\u0435 \u0442\u0430\u0439\u043c-\u0430\u0443\u0442\u044b.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/677\/d07\/cac\/677d07caca4267e5f0370f000c59a142.png\" width=\"657\" height=\"352\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/677\/d07\/cac\/677d07caca4267e5f0370f000c59a142.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/677\/d07\/cac\/677d07caca4267e5f0370f000c59a142.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e (\u043a\u043e\u0440\u043e\u0442\u043a\u043e \u0438 \u043d\u0430\u0434\u0451\u0436\u043d\u043e)<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u2014 Playwright<\/h4>\n<p>\u041f\u043e\u0447\u0435\u043c\u0443: \u0430\u0432\u0442\u043e\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u00ab\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438\u00bb, \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u044b, \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u043a\u0440\u043e\u043b\u043b, \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442 \u0441\u0435\u0442\u0438\/\u043a\u043e\u043d\u0441\u043e\u043b\u0438, \u043c\u0435\u043d\u044c\u0448\u0435 \u043a\u043e\u0434\u0430 \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 flaky.<\/p>\n<pre><code class=\"python\"># pip install playwright pytest-playwright # playwright install  from playwright.sync_api import Page  def click(page: Page, locator: str):     # Playwright \u0441\u0430\u043c \u0434\u043e\u0436\u0434\u0451\u0442\u0441\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438\/\u043a\u043b\u0438\u043a\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u0434\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442     page.locator(locator).click()  def type_text(page: Page, locator: str, text: str):     page.locator(locator).fill(text)  def get_text(page: Page, locator: str) -&gt; str:     return page.locator(locator).inner_text()  def click_in_scroll_container(page: Page, container: str):     container_locator = page.locator(container)     container_locator.scroll_into_view_if_needed()     container_locator.click() <\/code><\/pre>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u00ab\u0441\u0442\u0440\u0435\u043b\u043e\u043a \u0432\u043d\u0438\u0437\u00bb, <code>sleep(0.1)<\/code> \u0438 \u0448\u0430\u043c\u0430\u043d\u0441\u0442\u0432\u0430 \u0441 ActionChains.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u043a\u0430\u0442\u043e\u0440\u044b \u043b\u0443\u0447\u0448\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0435 XPath-\u00ab\u043f\u0440\u043e\u0441\u0442\u044b\u043d\u044f\u043c\u0438\u00bb, \u0430 \u0447\u0435\u0440\u0435\u0437 <code>data-test-id<\/code>:<br \/> <code>page.get_by_test_id(locator).click()<\/code>.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041a\u043e\u0433\u0434\u0430 \u0432\u0441\u0451-\u0442\u0430\u043a\u0438 Selenium?<\/h3>\n<p>\u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 \u0443\u0436\u0435 \u043d\u0430 <a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a> \u0438 \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f, \u0441\u0432\u043e\u0434\u0438\u043c \u0443\u0442\u0438\u043b\u0438\u0442\u044b \u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c\u0443 \u0438 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u043a\u043b\u0430\u0432\u0438\u0448\u0438 \u043a\u0430\u043a \u043a\u043e\u0441\u0442\u044b\u043b\u0438:<\/p>\n<pre><code class=\"python\">from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import ElementClickInterceptedException  def click(driver, xpath: str, timeout: int = 10) -&gt; None:     locator = (By.XPATH, xpath)     el = WebDriverWait(driver, timeout).until(EC.element_to_be_clickable(locator))     driver.execute_script(\"arguments[0].scrollIntoView({block:'center', inline:'center'})\", el)     try:         el.click()     except ElementClickInterceptedException:         driver.execute_script(\"arguments[0].click()\", el)  # \u0440\u0435\u0434\u043a\u0438\u0439 \u0440\u0435\u0437\u0435\u0440\u0432  def type_text(driver, xpath: str, text: str, timeout: int = 10) -&gt; None:     el = WebDriverWait(driver, timeout).until(EC.visibility_of_element_located((By.XPATH, xpath)))     el.clear()     el.send_keys(text) <\/code><\/pre>\n<h3>\u041a\u043e\u0433\u0434\u0430 \u0443\u043c\u0435\u0441\u0442\u043d\u044b \u043a\u043b\u0430\u0432\u0438\u0448\u0438?<\/h3>\n<p>\u0422\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0432\u044b \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c\/\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044e \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u043e\u0439 (Tab flow, \u043c\u0435\u043d\u044e-\u0441\u0442\u0440\u0435\u043b\u043a\u0438, \u0445\u043e\u0442\u043a\u0435\u0438). \u0414\u043b\u044f \u00ab\u043f\u0440\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442\u044c \u0438 \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c\u00bb \u2014 \u044d\u0442\u043e \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d.<\/p>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u00ab\u0442\u0430\u043d\u0446\u0435\u0432\u00bb<\/h3>\n<ul>\n<li>\n<p>Playwright \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0430\u0432\u0442\u043e\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f, \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u044b).<\/p>\n<\/li>\n<li>\n<p>\u0415\u0441\u043b\u0438 Selenium \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u044f\u0432\u043d\u044b\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f + <code>scrollIntoView<\/code>, \u0431\u0435\u0437 <code>sleep<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0434\u0438\u043d-\u0434\u0432\u0430 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0445 \u0445\u0435\u043b\u043f\u0435\u0440\u0430 \u0432\u043c\u0435\u0441\u0442\u043e \u0434\u0435\u0441\u044f\u0442\u043a\u043e\u0432 \u00ab\u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u0432\u043d\u0438\u0437 N \u0440\u0430\u0437\u00bb.<\/p>\n<\/li>\n<li>\n<p>JS-\u043a\u043b\u0438\u043a \u2014 \u043a\u0430\u043a \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, \u0430 \u043d\u0435 \u043a\u0430\u043a \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044f.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 2. \u00abexec \u0432 API\u00bb \u0438 \u043f\u0440\u043e\u0447\u0430\u044f \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043c\u0430\u0433\u0438\u044f<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0424\u0443\u043d\u043a\u0446\u0438\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c, \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u0438 \u043d\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0438.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">def post_request(requests_url: str, requests_body: dict, requests_headers: dict,                  pre_script: str = None, auth: list = None):     # \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 \u00ab\u0434\u043b\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438\u00bb \ud83e\udd26     if pre_script is not None:         exec(pre_script)      body = json.dumps(requests_body)     response = requests.post(requests_url, auth=auth, data=body, headers=requests_headers)      if response.status_code in (200, 201):         print('POST request successful')         return response.json()     else:         print('POST request failed')         return response.json()<\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p><code>exec(pre_script)<\/code> \u2014 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0438\u0437 \u0441\u0442\u0440\u043e\u043a\u0438. \u042d\u0442\u043e \u0443\u044f\u0437\u0432\u0438\u043c\u043e\u0441\u0442\u044c \u043a\u043b\u0430\u0441\u0441\u0430 RCE. \u0414\u043e\u0432\u0435\u0440\u0438\u0435 \u043a \u0434\u0430\u043d\u043d\u044b\u043c \u2260 \u043f\u043e\u0432\u043e\u0434 \u0438\u0445 \u0438\u0441\u043f\u043e\u043b\u043d\u044f\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0435\u0448\u0435\u043d\u0438\u0435 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u0412 \u043e\u0434\u043d\u043e\u043c \u043c\u0435\u0442\u043e\u0434\u0435 \u00ab\u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0430 \u043f\u0440\u0435\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0438\u043d\u0433\u0430\u00bb, \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f, \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0432\u044b\u0437\u043e\u0432 \u0438 \u00ab\u043c\u043e\u043b\u0447\u0430\u043b\u0438\u0432\u043e\u0435\u00bb \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.<\/p>\n<\/li>\n<li>\n<p><code>data=body<\/code> \u0432\u043c\u0435\u0441\u0442\u043e <code>json=...<\/code> \u2014 \u0440\u0438\u0441\u043a\u0443\u0435\u0442\u0435 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u043c <code>Content-Type<\/code> \u0438 \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u043e\u0439 (\u0438 \u0440\u0443\u0447\u043d\u043e\u0439 \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0442\u0430\u043c, \u0433\u0434\u0435 \u043e\u043d\u0430 \u043d\u0435 \u043d\u0443\u0436\u043d\u0430).<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u043e\u0432\/\u0440\u0435\u0442\u0440\u0430\u0435\u0432 \u2014 \u043f\u043e\u0434\u0432\u0438\u0441\u0430\u043d\u0438\u044f \u0438 flaky \u043d\u0430 CI.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u0442 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430. \u041d\u0435\u044f\u0441\u043d\u043e, \u0447\u0442\u043e \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u0435\u0442\u043e\u0434, \u043a\u0430\u043a \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c 4xx\/5xx.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/a2b\/32a\/0a8\/a2b32a0a841774b70e8818cd8217a138.png\" width=\"640\" height=\"320\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/a2b\/32a\/0a8\/a2b32a0a841774b70e8818cd8217a138.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/a2b\/32a\/0a8\/a2b32a0a841774b70e8818cd8217a138.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 1. \u041d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u00ab\u0447\u0438\u0441\u0442\u044b\u0439\u00bb \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 httpx<\/h4>\n<pre><code class=\"python\">import httpx   class HTTPClient:     def __init__(self, client: httpx.Client):         self.client = client      def post(self, url: str, json: dict, headers: dict | None = None) -&gt; httpx.Response:         return self.client.post(url, json=json, headers=headers)      def close(self):         self.client.close()  # \u043f\u0440\u0438\u043c\u0435\u0440 client = HTTPClient(httpx.Client(timeout=5)) resp = client.post(\"https:\/\/api.example.com\/login\", json={\"user\": \"foo\"}) print(resp.status_code, resp.json())<\/code><\/pre>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 2. \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 + \u0440\u0435\u0442\u0440\u0430\u0438 (\u043a\u043e\u0440\u043e\u0442\u043a\u043e)<\/h4>\n<pre><code class=\"python\">import httpx from backoff import on_exception, expo  # pip install backoff   class HTTPClient:     def __init__(self, client: httpx.AsyncClient):         self.client = client      @on_exception(expo, (httpx.TimeoutException, httpx.ConnectError), max_tries=3)     async def post(self, url: str, json: dict, headers: dict | None = None) -&gt; httpx.Response:         return await self.client.post(url, json=json, headers=headers)      async def aclose(self):         await self.client.aclose()<\/code><\/pre>\n<h4>(\u041e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e) \u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0447\u0435\u0440\u0435\u0437 Pydantic<\/h4>\n<pre><code class=\"python\">from pydantic import BaseModel  class LoginRequest(BaseModel):     username: str     password: str  class LoginResponse(BaseModel):     access_token: str     token_type: str = \"Bearer\"  # \u043f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f payload = LoginRequest(username=\"foo\", password=\"bar\").model_dump() resp = client.post(\"https:\/\/api.example.com\/login\", json=payload) parsed = LoginResponse.model_validate_json(resp.text)<\/code><\/pre>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0438 \u0437\u0434\u0440\u0430\u0432\u043e\u0433\u043e \u0441\u043c\u044b\u0441\u043b\u0430<\/h3>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 <a href=\"https:\/\/docs.python.org\/3\/library\/functions.html#exec\" rel=\"noopener noreferrer nofollow\">exec<\/a>, <a href=\"https:\/\/docs.python.org\/3\/library\/functions.html#eval\" rel=\"noopener noreferrer nofollow\">eval<\/a>, \u00ab\u043f\u0440\u0435\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432\u00bb \u0441\u0442\u0440\u043e\u043a\u043e\u0439.<\/p>\n<\/li>\n<li>\n<p>\u0421\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u2014 \u0447\u0435\u0440\u0435\u0437 <code>json=<\/code>; \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0437\u0430\u0434\u0430\u0451\u043c \u044f\u0432\u043d\u043e, \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0441\u0435\u0433\u0434\u0430 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u044b; \u0434\u043b\u044f \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u0442\u0435\u0439 \u2014 \u0440\u0435\u0442\u0440\u0430\u0438 \u0441 \u044d\u043a\u0441\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0439.<\/p>\n<\/li>\n<li>\n<p>\u0415\u0434\u0438\u043d\u044b\u0439 \u0438 \u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0443\u0435\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 (\u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f).<\/p>\n<\/li>\n<li>\n<p>\u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0432\u0445\u043e\u0434\u0430\/\u0432\u044b\u0445\u043e\u0434\u0430 (<a href=\"https:\/\/docs.pydantic.dev\/latest\/\" rel=\"noopener noreferrer nofollow\">Pydantic<\/a>) \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 \u0441\u044e\u0440\u043f\u0440\u0438\u0437\u043e\u0432 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0433\u0438: \u043c\u0435\u0442\u043e\u0434, URL, \u0441\u0442\u0430\u0442\u0443\u0441, latency (\u0431\u0435\u0437 \u0443\u0442\u0435\u0447\u0435\u043a \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445).<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c <a href=\"https:\/\/peps.python.org\/pep-0760\/\" rel=\"noopener noreferrer nofollow\">bare except<\/a>: \u043b\u043e\u0432\u0438\u0442\u0435 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a>\/<a href=\"https:\/\/requests.readthedocs.io\/en\/latest\/\" rel=\"noopener noreferrer nofollow\">requests<\/a><\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 3. \u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 connection\/cursor<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0411\u0414 \u0438 \u043a\u0443\u0440\u0441\u043e\u0440 \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u00ab\u0433\u0434\u0435-\u0442\u043e \u0441\u0432\u0435\u0440\u0445\u0443\u00bb, \u043a\u043b\u0430\u0434\u0443\u0442\u0441\u044f \u0432 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0438 \u0434\u0430\u043b\u044c\u0448\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0438\u0437 \u043b\u044e\u0431\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\"># \u0433\u0434\u0435-\u0442\u043e \u0432 \u043c\u043e\u0434\u0443\u043b\u0435 global connection, cursor connection = psycopg2.connect(host=..., user=..., password=..., dbname=...) cursor = connection.cursor()  def export_table(...):     cursor.execute(sql)          # \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u0443\u0440\u0441\u043e\u0440     rows = cursor.fetchall()     ... # \u0432 finally \u0433\u0434\u0435-\u043d\u0438\u0431\u0443\u0434\u044c \u043d\u0438\u0436\u0435: cursor.close(); connection.close()<\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>\u0423\u0442\u0435\u0447\u043a\u0438 \u0438 \u00ab\u0432\u0438\u0441\u044f\u0449\u0438\u0435\u00bb \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438. \u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u043d\u0435\u043a\u0442 \u043b\u0435\u0433\u043a\u043e \u0437\u0430\u0431\u044b\u0442\u044c \u0437\u0430\u043a\u0440\u044b\u0442\u044c; \u0430\u0432\u0442\u043e\u043a\u043e\u043c\u043c\u0438\u0442 \/ \u043d\u0435\u044f\u0432\u043d\u044b\u0435 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432\u0438\u0441\u044f\u0442 \u043c\u0435\u0436\u0434\u0443 \u0442\u0435\u0441\u0442\u0430\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u043f\u043e\u0442\u043e\u043a\u043e\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e. \u041f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u043f\u0443\u0441\u043a (<a href=\"https:\/\/github.com\/pytest-dev\/pytest-xdist\" rel=\"noopener noreferrer nofollow\">pytest-xdist<\/a>) \u0438\u043b\u0438 \u043f\u0440\u043e\u0441\u0442\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0442\u0435\u0441\u0442\u043e\u0432 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u2014 \u0438 \u0432\u044b \u043b\u043e\u0432\u0438\u0442\u0435 \u0433\u043e\u043d\u043a\u0438\/\u00ab\u043a\u0443\u0440\u0441\u043e\u0440 \u0443\u0436\u0435 \u0437\u0430\u043a\u0440\u044b\u0442\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u0438\u0437\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b. \u041e\u0434\u0438\u043d \u0442\u0435\u0441\u0442 \u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0411\u0414 \u2014 \u0434\u0440\u0443\u0433\u043e\u0439 \u0432\u0438\u0434\u0438\u0442 \u043c\u0443\u0441\u043e\u0440.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u043b\u044c\u0437\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u0447\u0435\u0447\u043d\u043e. \u0425\u043e\u0442\u0438\u0442\u0435 \u0438\u043d\u043e\u0439 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\/\u0440\u043e\u043b\u044c\/\u0441\u0445\u0435\u043c\u0443 \u2014 \u0443\u0432\u044b, \u00ab\u0443 \u043d\u0430\u0441 \u043e\u0434\u0438\u043d \u043d\u0430 \u0432\u0441\u0435\u0445\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u043f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438. \u041e\u0448\u0438\u0431\u043a\u0430 \u00ab\u0433\u0434\u0435-\u0442\u043e\u00bb \u0432 \u043e\u0431\u0449\u0435\u043c \u043a\u0443\u0440\u0441\u043e\u0440\u0435 \u2192 \u043f\u0430\u0434\u0430\u0442\u044c \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u00ab\u0432\u0441\u0451\u00bb \u0438 \u043e\u0442\u043b\u0430\u0436\u0438\u0432\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u043d\u043e.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/aa8\/ff7\/d18\/aa8ff7d1844c7e20760c81a9caad2181.png\" width=\"955\" height=\"774\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/aa8\/ff7\/d18\/aa8ff7d1844c7e20760c81a9caad2181.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/aa8\/ff7\/d18\/aa8ff7d1844c7e20760c81a9caad2181.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 A. \u0427\u0438\u0441\u0442\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f + \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u044b\u0435 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u044b (psycopg2)<\/h4>\n<pre><code class=\"python\">import psycopg2 from typing import Any, Iterable  ConnParams = dict[str, Any]  def fetch_all(params: ConnParams, sql: str, args: Iterable[Any] | None = None):     \"\"\"\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435\/\u043a\u0443\u0440\u0441\u043e\u0440, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441, \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0440\u0435\u0441\u0443\u0440\u0441\u044b.\"\"\"     with psycopg2.connect(**params) as conn:         with conn.cursor() as cur:             cur.execute(sql, args)             return cur.fetchall()  # \u0430\u0432\u0442\u043e\u043a\u043e\u043c\u043c\u0438\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a; \u0434\u043b\u044f SELECT \u044d\u0442\u043e \u043e\u043a  def execute(params: ConnParams, sql: str, args: Iterable[Any] | None = None) -&gt; int:     \"\"\"\u0414\u043b\u044f INSERT\/UPDATE\/DELETE \u2014 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0447\u0438\u0441\u043b\u043e \u0437\u0430\u0442\u0440\u043e\u043d\u0443\u0442\u044b\u0445 \u0441\u0442\u0440\u043e\u043a, \u043a\u043e\u043c\u043c\u0438\u0442\u0438\u0442 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e.\"\"\"     with psycopg2.connect(**params) as conn:         with conn.cursor() as cur:             cur.execute(sql, args)             return cur.rowcount <\/code><\/pre>\n<ul>\n<li>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0432\u044b\u0437\u043e\u0432 \u0441\u0430\u043c \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0440\u0435\u0441\u0443\u0440\u0441\u0430\u043c\u0438 \u2014 \u043d\u0435\u0442 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 <code>args<\/code> \u2014 \u0437\u0430\u0449\u0438\u0442\u0430 \u043e\u0442 SQL-\u0438\u043d\u044a\u0435\u043a\u0446\u0438\u0439 (\u043d\u0438\u043a\u0430\u043a\u0438\u0445 <code>f\"... {user_id} ...\"<\/code>).<\/p>\n<\/li>\n<li>\n<p>\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f (<code>host<\/code>, <code>dbname<\/code>, <code>options<\/code>\/<code>search_path<\/code>) \u2014 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u0432\u044b\u0437\u043e\u0432\u0430.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 B. \u041f\u0443\u043b \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 (\u0435\u0441\u043b\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043c\u043d\u043e\u0433\u043e)<\/h4>\n<pre><code class=\"python\">from psycopg2.pool import ThreadedConnectionPool  pool = ThreadedConnectionPool(minconn=1, maxconn=10, **conn_params)  def run_in_pool(sql: str, args=None):     conn = pool.getconn()     try:         with conn, conn.cursor() as cur:             cur.execute(sql, args)             return cur.fetchall() if cur.description else cur.rowcount     finally:         pool.putconn(conn) <\/code><\/pre>\n<ul>\n<li>\n<p>\u041f\u043e\u0434\u043e\u0439\u0434\u0451\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0440\u0430\u043d\u043d\u0435\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0447\u0430\u0441\u0442\u043e \u0445\u043e\u0434\u044f\u0442 \u0432 \u0411\u0414.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0441\u0451 \u0435\u0449\u0451 \u0431\u0435\u0437 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u0443\u0440\u0441\u043e\u0440\u0430 \u0438 \u0441 \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u044b\u043c \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u043e\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 C. SQLAlchemy (\u0438\u043d\u0434\u0443\u0441\u0442\u0440\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442)<\/h4>\n<pre><code class=\"python\">from sqlalchemy import create_engine, text engine = create_engine(\"postgresql+psycopg2:\/\/user:pass@host:5432\/dbname\", future=True)  def fetch_sa(sql: str, **params):     with engine.connect() as conn:         res = conn.execute(text(sql), params)         return res.mappings().all()  # \u0441\u043f\u0438\u0441\u043e\u043a dict\u2019\u043e\u0432<\/code><\/pre>\n<ul>\n<li>\n<p>\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043a\u0440\u043e\u0441\u0441-\u0421\u0423\u0411\u0414, \u0443\u0434\u043e\u0431\u043d\u044b\u0435 \u043c\u0430\u043f\u043f\u0438\u043d\u0433\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0441\u043b\u043e\u0436\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u2014 ORM\/\u043c\u043e\u0434\u0435\u043b\u0438, \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 Alembic.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0422\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0438\u0437\u043e\u043b\u044f\u0446\u0438\u044f (\u043e\u0447\u0435\u043d\u044c \u0432\u0430\u0436\u043d\u043e)<\/h3>\n<p>\u0427\u0442\u043e\u0431\u044b \u0442\u0435\u0441\u0442\u044b \u043d\u0435 \u043f\u0430\u0447\u043a\u0430\u043b\u0438 \u0411\u0414 \u0438 \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0435\u043b\u0438 \u0434\u0440\u0443\u0433 \u043e\u0442 \u0434\u0440\u0443\u0433\u0430 \u2014 \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u043c \u043a\u0430\u0436\u0434\u044b\u0439 \u0442\u0435\u0441\u0442 \u0432 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e \u0438 \u043e\u0442\u043a\u0430\u0442\u044b\u0432\u0430\u0435\u043c \u0435\u0451.<\/p>\n<h4>Pytest-\u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b \u043d\u0430 psycopg2<\/h4>\n<pre><code class=\"python\">import psycopg2, pytest  @pytest.fixture(scope=\"session\") def db_conn(params):     conn = psycopg2.connect(**params)     conn.autocommit = False     yield conn     conn.close()  @pytest.fixture def db_cur(db_conn):     cur = db_conn.cursor()     db_conn.begin()          # \u043d\u043e\u0432\u0430\u044f \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f     try:         yield cur           # \u0442\u0435\u0441\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 SQL \u0437\u0434\u0435\u0441\u044c         db_conn.rollback()  # \u043e\u0442\u043a\u0430\u0442\u0438\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043f\u043e\u0441\u043b\u0435 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u0430     finally:         cur.close() <\/code><\/pre>\n<ul>\n<li>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0442\u0435\u0441\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 <strong>\u0447\u0438\u0441\u0442\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435<\/strong>, \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435 \u00ab\u043f\u0440\u043e\u0442\u0435\u043a\u0430\u044e\u0442\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u041c\u043e\u0436\u043d\u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u044c <code>SAVEPOINT<\/code>\/<code>begin_nested<\/code> \u0434\u043b\u044f \u0431\u043e\u043b\u0435\u0435 \u0442\u043e\u043d\u043a\u043e\u0439 \u0433\u0440\u0430\u043d\u0443\u043b\u044f\u0446\u0438\u0438.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 <code>global connection, cursor<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0441\u0435\u0433\u0434\u0430 <code>with connect() as conn, conn.cursor() as cur:<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b (<code>cur.execute(sql, args)<\/code>), \u043d\u0435 f-\u0441\u0442\u0440\u043e\u043a\u0438 \u0441 \u0434\u0430\u043d\u043d\u044b\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u043c\u0430\u0441\u0441\u043e\u0432\u044b\u0445 \u0432\u044b\u0437\u043e\u0432\u043e\u0432 \u2014 \u043f\u0443\u043b \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u2014 <a href=\"https:\/\/www.sqlalchemy.org\/\" rel=\"noopener noreferrer nofollow\">SQLAlchemy<\/a> (Core\/ORM) + \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0412 \u0442\u0435\u0441\u0442\u0430\u0445 \u2014 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u043d\u0430 \u0442\u0435\u0441\u0442 \u0438 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 rollback.<\/p>\n<\/li>\n<li>\n<p>\u0420\u0430\u0437\u043d\u044b\u0435 \u0421\u0423\u0411\u0414 \u2014 \u0440\u0430\u0437\u043d\u044b\u0435 \u043c\u043e\u0434\u0443\u043b\u0438\/\u043a\u043b\u0438\u0435\u043d\u0442\u044b, \u043d\u0435 \u00ab\u0432\u0441\u0451 \u0432 \u043e\u0434\u043d\u043e\u043c \u043a\u043b\u0430\u0441\u0441\u0435\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u0420\u0430\u0437\u0434\u0435\u043b\u044f\u0439\u0442\u0435 \u043a\u0440\u0435\u0434\u044b\/DSN \u0447\u0435\u0440\u0435\u0437 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f (\u043d\u0435 \u0445\u0430\u0440\u0434\u043a\u043e\u0434\u0438\u043c \u0432 \u043a\u043e\u0434\u0435).<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 4. \u00ab\u0422\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u0430\u043c\u0430 \u0441\u0442\u0430\u0432\u0438\u0442 Node.js\/Newman \u0447\u0435\u0440\u0435\u0437 sudo\u00bb<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412\u043d\u0443\u0442\u0440\u0438 \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430 \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432\u00bb \u0435\u0441\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043b\u0435\u0437\u0435\u0442 \u0432 \u041e\u0421 \u0438 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0435 \u043f\u0430\u043a\u0435\u0442\u044b \u2014 Node.js, npm \u0438 Newman \u2014 \u043f\u0440\u0438\u0447\u0451\u043c \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043f\u0443\u0442\u044f\u043c\u0438 \u0434\u043b\u044f Windows\/macOS\/Linux, \u043c\u0435\u0441\u0442\u0430\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 <code>sudo<\/code>, \u043c\u0435\u0441\u0442\u0430\u043c\u0438 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u044f MSI.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">def install_newman():     def is_tool_installed(tool):         subprocess.run([tool, \"--version\"], check=True)      def install_nodejs():         if sys.platform.startswith(\"win\"):             subprocess.run([\"curl\", \"-o\", \"node.msi\", NODE_URL], check=True)             subprocess.run([\"msiexec\", \"\/i\", \"node.msi\", \"\/quiet\", \"\/norestart\"], check=True)         elif sys.platform.startswith(\"darwin\"):             subprocess.run([\"brew\", \"install\", \"node\"], check=True)         elif sys.platform.startswith(\"linux\"):             distro = subprocess.run([\"lsb_release\", \"-is\"], stdout=PIPE).stdout.decode().strip().lower()             if distro in [\"ubuntu\", \"debian\"]:                 subprocess.run([\"sudo\", \"apt\", \"update\"], check=True)                 subprocess.run([\"sudo\", \"apt\", \"install\", \"-y\", \"nodejs\"], check=True)             # ... \u0438 \u0442.\u0434.      if not is_tool_installed(\"node\"):         install_nodejs()      if not is_tool_installed(\"npm\"):         # \u0435\u0449\u0451 \u043e\u0434\u043d\u0430 \u0432\u0435\u0442\u043a\u0430 \u0441 \u043f\u0430\u043a\u0435\u0442\u043d\u044b\u043c\u0438 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430\u043c\u0438 \u0438 sudo         ...      if not is_tool_installed(\"newman\"):         subprocess.run([\"npm\", \"install\", \"-g\", \"newman\"], check=True) <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>\u041d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u0433\u0440\u0430\u043d\u0438\u0446 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u0422\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u041e\u0421. \u042d\u0442\u043e \u0437\u0430\u0434\u0430\u0447\u0430 DevOps\/\u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f, \u0430 \u043d\u0435 \u043a\u043e\u0434\u0430 \u0432 <code>framework_for_tests.py<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c. <code>sudo<\/code>, \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u0435 \u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0431\u0438\u043d\u0430\u0440\u0435\u0439 \u00ab\u043d\u0430 \u043b\u0435\u0442\u0443\u00bb \u0438\u0437 \u0442\u0435\u0441\u0442\u043e\u0432 \u2014 \u044d\u0442\u043e \u043f\u0440\u044f\u043c\u043e\u0435 \u043f\u0440\u0438\u0433\u043b\u0430\u0448\u0435\u043d\u0438\u0435 \u043a RCE\/\u043f\u043e\u0440\u0447\u0435 \u043c\u0430\u0448\u0438\u043d\u044b.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u043c\u043e\u0441\u0442\u044c. \u0421\u0435\u0433\u043e\u0434\u043d\u044f <code>apt install nodejs<\/code> \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u043b v18, \u0437\u0430\u0432\u0442\u0440\u0430 v22. \u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u00ab\u0442\u0435\u0441\u0442\u043e\u0432\u00bb \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0437\u043d\u044b\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u043c\u0430\u0435\u0442 CI\/CD. \u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b \u0441\u043e\u0431\u0440\u0430\u043d\u044b \u0437\u0430\u0440\u0430\u043d\u0435\u0435. \u041b\u044e\u0431\u0430\u044f \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0441\u043e\u0444\u0442 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0442\u0435\u0441\u0442\u0430 \u2014 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u043e, \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e \u0438 \u0447\u0430\u0441\u0442\u043e \u043f\u043e\u043f\u0440\u043e\u0441\u0442\u0443 \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043a\u0440\u044b\u0442\u044b\u0435 \u043f\u043e\u0431\u043e\u0447\u043a\u0438. \u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u0430\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 <code>npm -g newman<\/code> \u043c\u0435\u043d\u044f\u0435\u0442 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\/\u0430\u0433\u0435\u043d\u0442\u0430. \u041e\u0442\u043a\u0430\u0442\u044b \u043d\u0435\u0442.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/209\/e1c\/c1b\/209e1cc1b9ac6ebc0994d94766ee961d.png\" width=\"596\" height=\"556\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/209\/e1c\/c1b\/209e1cc1b9ac6ebc0994d94766ee961d.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/209\/e1c\/c1b\/209e1cc1b9ac6ebc0994d94766ee961d.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 A. \u041a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u0441 \u0437\u0430\u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044f\u043c\u0438 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)<\/h4>\n<p>Dockerfile (\u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442):<\/p>\n<pre><code class=\"python\">FROM python:3.12-slim  # 1) Python-\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 COPY requirements.txt . RUN pip install -r requirements.txt  # 2) Node.js + Newman (\u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0432\u0435\u0440\u0441\u0438\u0438) RUN apt-get update &amp;&amp; apt-get install -y curl ca-certificates gnupg \\  &amp;&amp; mkdir -p \/etc\/apt\/keyrings \\  &amp;&amp; curl -fsSL https:\/\/deb.nodesource.com\/gpgkey\/nodesource-repo.gpg.key \\     | gpg --dearmor -o \/etc\/apt\/keyrings\/nodesource.gpg \\  &amp;&amp; echo \"deb [signed-by=\/etc\/apt\/keyrings\/nodesource.gpg] https:\/\/deb.nodesource.com\/node_18.x nodistro main\" \\     &gt; \/etc\/apt\/sources.list.d\/nodesource.list \\  &amp;&amp; apt-get update &amp;&amp; apt-get install -y nodejs \\  &amp;&amp; npm install -g newman@5.3.2 \\  &amp;&amp; apt-get clean &amp;&amp; rm -rf \/var\/lib\/apt\/lists\/*  WORKDIR \/app COPY . .<\/code><\/pre>\n<ul>\n<li>\n<p>\u0412\u0441\u0451 \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044f \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u0441\u0431\u043e\u0440\u043a\u0438, \u0432\u0435\u0440\u0441\u0438\u0438 \u0437\u0430\u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u044b.<\/p>\n<\/li>\n<li>\n<p>\u0412 \u0440\u0430\u043d\u0442\u0430\u0439\u043c\u0435 \u0442\u0435\u0441\u0442\u044b \u043d\u0435 \u043b\u0435\u0437\u0443\u0442 \u0432 \u041e\u0421.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 B. Make\/CI-\u043e\u0440\u043a\u0435\u0441\u0442\u0440\u0430\u0446\u0438\u044f \u2014 \u043d\u0435 \u0438\u0437 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u043b\u0438\u0431\u044b<\/h4>\n<p>Makefile (\u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442):<\/p>\n<pre><code class=\"python\">deps: \\tpip install -r requirements.txt \\tnpm install -g newman@5.3.2  test: \\tpytest -m \"not e2e\"  perf: \\tnewman run perf_collection.json -e perf_env.json<\/code><\/pre>\n<ul>\n<li>\n<p>\u0418\u043d\u0441\u0442\u0430\u043b\u043b\u044f\u0446\u0438\u044f \u0438 \u0437\u0430\u043f\u0443\u0441\u043a \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u0446\u0435\u043b\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0422\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 <strong>\u043d\u0435 \u0437\u043d\u0430\u0435\u0442<\/strong> \u043f\u0440\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0445 \u0443\u0442\u0438\u043b\u0438\u0442.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 C. \u0415\u0441\u043b\u0438 Newman \u043e\u0447\u0435\u043d\u044c \u043d\u0443\u0436\u0435\u043d \u2014 \u043e\u0431\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0439\u0442\u0435 \u0432\u044b\u0437\u043e\u0432 \u0441 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u043e\u0439, \u043d\u043e \u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0439\u0442\u0435<\/h4>\n<pre><code class=\"python\">import shutil, subprocess  def run_newman(collection: str, env: str) -&gt; int:     newman = shutil.which(\"newman\")     if not newman:         raise RuntimeError(\"newman is not installed. Please install it via Docker image or CI step.\")     return subprocess.run([newman, \"run\", collection, \"-e\", env], check=False).returncode<\/code><\/pre>\n<ul>\n<li>\n<p>\u042f\u0432\u043d\u043e \u043f\u0430\u0434\u0430\u0435\u043c \u0441 \u043f\u043e\u043d\u044f\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u043e\u0439, \u0435\u0441\u043b\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043d\u0435\u0442.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 <code>sudo<\/code> \u0438 \u00ab\u043c\u0430\u0433\u0438\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438\u00bb.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 Newman: \u0447\u0438\u0441\u0442\u044b\u0439 Python \u0438\u043b\u0438 \u043f\u0440\u043e\u0444\u0438\u043b\u044c\u043d\u044b\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b<\/h3>\n<ul>\n<li>\n<p>\u0414\u043b\u044f API \u2014 <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a> + <a href=\"https:\/\/docs.pytest.org\/en\/stable\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a> (\u0438 \u043e\u0442\u0447\u0451\u0442\u043d\u043e\u0441\u0442\u044c Allure).<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u2014 <a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a>\/<a href=\"https:\/\/k6.io\/\" rel=\"noopener noreferrer nofollow\">k6<\/a> (\u043c\u0435\u0442\u0440\u0438\u043a\u0438, \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438, \u043f\u0440\u043e\u0444\u0438\u043b\u0438).<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f E2E \u2014 <a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a>, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0435\u0441\u0442\u044c \u0442\u0440\u0435\u0439\u0441\u0438\u043d\u0433\/\u0432\u0438\u0434\u0435\u043e, \u0441\u0435\u0442\u0435\u0432\u044b\u0435 HAR \u0431\u0435\u0437 \u043f\u043b\u044f\u0441\u043e\u043a.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0441\u0442\u0430\u0432\u0438\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0441\u043e\u0444\u0442 \u0438\u0437 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0441\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u2014 \u0432 <a href=\"https:\/\/docs.docker.com\/reference\/dockerfile\/\" rel=\"noopener noreferrer nofollow\">Dockerfile<\/a> \u0438\u043b\u0438 \u0432 CI \u0448\u0430\u0433\u0435.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0435\u0440\u0441\u0438\u0438 \u0444\u0438\u043a\u0441\u0438\u0440\u0443\u0435\u043c (lockfile\/\u0442\u0435\u0433\u0438).<\/p>\n<\/li>\n<li>\n<p>\u0412 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u043c \u043a\u043e\u0434\u0435 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u044f \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0430 \u0438 \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u044b\u0439 \u0432\u044b\u0437\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u043c \u00ab\u0432\u043d\u0435\u0448\u043d\u0438\u0435 CLI\u00bb \u043d\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u0447\u043d\u044b\u0435 \u0432\u044b\u0437\u043e\u0432\u044b \u0432 <a href=\"https:\/\/www.python.org\/\" rel=\"noopener noreferrer nofollow\">Python<\/a>\/<a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a>\/<a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a>.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 5. \u00ab\u041d\u0430\u0433\u0440\u0443\u0437\u043a\u0430\u00bb \u0447\u0435\u0440\u0435\u0437 httpx \u0438 pytest.mark.asyncio<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0424\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u00ab\u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u043c \u0442\u0435\u0441\u0442\u043e\u043c\u00bb \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0430\u0447\u043a\u0443 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u044b\u0445 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 <a href=\"https:\/\/docs.python.org\/3\/library\/asyncio-task.html\" rel=\"noopener noreferrer nofollow\">asyncio.gather<\/a>, \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0445 <a href=\"https:\/\/github.com\/pytest-dev\/pytest-asyncio\" rel=\"noopener noreferrer nofollow\">@pytest.mark.asyncio<\/a>. \u0413\u0434\u0435-\u0442\u043e \u043f\u0435\u0447\u0430\u0442\u0430\u0435\u0442\u0441\u044f \u0442\u0435\u043a\u0441\u0442 \u00abCount = N\u00bb, \u0438 \u043d\u0430 \u044d\u0442\u043e\u043c \u00ab\u043f\u0435\u0440\u0444\u043e\u043c\u0430\u043d\u0441\u00bb \u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">class Load:     @staticmethod     @pytest.mark.asyncio     async def make_get_request(url):         async with httpx.AsyncClient() as client:             response = await client.get(url)             response.raise_for_status()             return url, response.text      @staticmethod     @pytest.mark.asyncio     async def concurrent_get_requests(urls):         tasks = [Load.make_get_request(u) for u in urls]         return await asyncio.gather(*tasks)      @staticmethod     async def run_load_method_of_get_requests(url, count):         urls = [url] * count         results = await Load.concurrent_get_requests(urls)         for u, text in results:             print(f\"Count = {count}, URL: {u}, Response: {text}\") <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>\u042d\u0442\u043e \u043d\u0435 \u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u043e\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435. \u041d\u0435\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (RPS\/Concurrency\/Duration), \u043d\u0435\u0442 \u043f\u0440\u043e\u0433\u0440\u0435\u0432\u0430, \u043d\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043b\u0438\u0437\u0430\u0446\u0438\u0438, \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u0439 latency\/percentiles, \u043d\u0435\u0442 \u043e\u0448\u0438\u0431\u043e\u043a\/\u0442\u0430\u0439\u043c\u0430\u0443\u0442\u043e\u0432 \u0432 \u043e\u0442\u0447\u0451\u0442\u0435, \u043d\u0435\u0442 \u043a\u043e\u0440\u0440\u0435\u043b\u044f\u0446\u0438\u0438 \u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0430\u043c\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (CPU, \u043f\u0430\u043c\u044f\u0442\u044c, \u0441\u0435\u0442\u044c).<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0435\u0448\u0435\u043d\u0438\u0435 \u0441 <a href=\"https:\/\/docs.pytest.org\/en\/stable\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a>. \u041d\u0430\u0432\u0435\u0448\u0438\u0432\u0430\u043d\u0438\u0435 <a href=\"https:\/\/github.com\/pytest-dev\/pytest-asyncio\" rel=\"noopener noreferrer nofollow\">@pytest.mark.asyncio<\/a> \u043d\u0430 \u0443\u0442\u0438\u043b\u0438\u0442\u044b \u043b\u043e\u043c\u0430\u0435\u0442 \u0437\u0430\u043f\u0443\u0441\u043a \u0432\u043d\u0435 <a href=\"https:\/\/docs.pytest.org\/en\/stable\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a> \u0438 \u00ab\u043f\u0440\u0438\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442\u00bb \u043a\u043e\u0434 \u043a \u0440\u0430\u043d\u043d\u0435\u0440\u0443 \u0442\u0435\u0441\u0442\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0432\u0440\u0435\u043c\u0435\u043d\u0438. <a href=\"https:\/\/docs.python.org\/3\/library\/asyncio-task.html\" rel=\"noopener noreferrer nofollow\">asyncio.gather<\/a> \u043c\u0430\u043a\u0441\u0438\u043c\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u0438\u0437\u043c \u00ab\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u043f\u0435\u043b\u0438\u00bb, \u043d\u043e \u043d\u0435 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u044c (RPS\/\u043a\u043e\u043d\u043a\u0430\u0440\u0440\u0435\u043d\u0441\u0438\/\u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c), \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e p95\/p99 \u0438 <a href=\"https:\/\/ru.wikipedia.org\/wiki\/%D0%A1%D0%BE%D0%B3%D0%BB%D0%B0%D1%88%D0%B5%D0%BD%D0%B8%D0%B5_%D0%BE%D0%B1_%D1%83%D1%80%D0%BE%D0%B2%D0%BD%D0%B5_%D1%83%D1%81%D0%BB%D1%83%D0%B3\" rel=\"noopener noreferrer nofollow\">SLA<\/a> \u043d\u0435\u0440\u0435\u043f\u0440\u0435\u0437\u0435\u043d\u0442\u0430\u0442\u0438\u0432\u043d\u044b.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u0440\u0435\u0430\u043b\u0438\u0441\u0442\u0438\u0447\u043d\u044b\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439. \u041d\u0435\u0442 \u0441\u0435\u0441\u0441\u0438\u0439, \u043a\u0443\u043a\u043e\u0432, \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u0432, \u0432\u0430\u0440\u0438\u0430\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438 payload\u2019\u043e\u0432, \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u043c\u0435\u0436\u0434\u0443 \u0448\u0430\u0433\u0430\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u043e\u0439 \u043e\u0442\u0447\u0451\u0442\u043d\u043e\u0441\u0442\u0438. \u041f\u0435\u0447\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u2014 \u044d\u0442\u043e \u043d\u0435 \u0440\u0435\u043f\u043e\u0440\u0442. \u041d\u0443\u0436\u043d\u044b \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b: p50\/p90\/p99, \u043e\u0448\u0438\u0431\u043a\u0438 \u043f\u043e \u043a\u043e\u0434\u0430\u043c, \u043f\u0435\u0440-\u0437\u0430\u043f\u0440\u043e\u0441\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430, \u0433\u0440\u0430\u0444\u0438\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6fb\/622\/4e9\/6fb6224e9e71ba19548b2e104a553773.png\" width=\"805\" height=\"449\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/6fb\/622\/4e9\/6fb6224e9e71ba19548b2e104a553773.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6fb\/622\/4e9\/6fb6224e9e71ba19548b2e104a553773.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0444\u0438\u043b\u044c\u043d\u044b\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u043e\u0432\u0430\u043d\u043e)<\/h4>\n<p>Locust (Python, \u0441\u0446\u0435\u043d\u0430\u0440\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434):<\/p>\n<pre><code class=\"python\"># pip install locust from locust import HttpUser, task, between  class WebsiteUser(HttpUser):     wait_time = between(0.5, 2.0)  # \u00ab\u0434\u0443\u043c-\u0442\u0430\u0439\u043c\u044b\u00bb \u043c\u0435\u0436\u0434\u0443 \u0448\u0430\u0433\u0430\u043c\u0438      @task(3)     def view_items(self):         self.client.get(\"\/items\", name=\"GET \/items\")      @task(1)     def create_item(self):         self.client.post(\"\/items\", json={\"name\": \"foo\"}, name=\"POST \/items\") <\/code><\/pre>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a \u0441 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u043c \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438:<\/p>\n<pre><code>locust -H https:\/\/api.example.com --headless -u 200 -r 20 -t 10m<\/code><\/pre>\n<ul>\n<li>\n<p><code>-u 200<\/code>: \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e 200 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439,<\/p>\n<\/li>\n<li>\n<p><code>-r 20<\/code>: \u0440\u0430\u0437\u0433\u043e\u043d \u043f\u043e 20 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439\/\u0441\u0435\u043a,<\/p>\n<\/li>\n<li>\n<p><code>-t 10m<\/code>: \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c 10 \u043c\u0438\u043d\u0443\u0442.<\/p>\n<\/li>\n<\/ul>\n<p><a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a> \u043e\u0442\u0434\u0430\u0451\u0442 p50\/p90\/p95\/p99, RPS, \u043e\u0448\u0438\u0431\u043a\u0438, \u043c\u043e\u0436\u043d\u043e \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c CSV \u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441 Grafana\/Prometheus.<\/p>\n<p><a href=\"https:\/\/k6.io\/\" rel=\"noopener noreferrer nofollow\">k6<\/a> (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430): \u0434\u0435\u043a\u043b\u0430\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438, \u043e\u0442\u043b\u0438\u0447\u043d\u044b\u0439 \u0432\u044b\u0432\u043e\u0434 \u043c\u0435\u0442\u0440\u0438\u043a \u0438 \u0443\u0434\u043e\u0431\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Grafana\/InfluxDB.<\/p>\n<h3>\u0427\u0442\u043e \u0435\u0449\u0451 \u0432\u0430\u0436\u043d\u043e \u0434\u043b\u044f \u00ab\u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0439 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438\u00bb<\/h3>\n<ul>\n<li>\n<p>\u0421\u0435\u0430\u043d\u0441\u044b \u0438 \u0434\u0430\u043d\u043d\u044b\u0435. \u0420\u0435\u0430\u043b\u0438\u0441\u0442\u0438\u0447\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438\/\u043a\u0443\u043a\u0438\/\u0442\u043e\u043a\u0435\u043d\u044b, \u0432\u0430\u0440\u0438\u0430\u0442\u0438\u0432\u043d\u044b\u0435 payload\u2019\u044b, \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u044b\/\u0441\u0438\u0434\u0438\u043d\u0433.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0435\u043c\u043e\u0441\u0442\u044c. \u041a\u043e\u0440\u0440\u0435\u043b\u044f\u0446\u0438\u044f RPS\/latency \u0441 CPU\/Memory\/GC\/DB\/Cache. \u0411\u0435\u0437 \u044d\u0442\u043e\u0433\u043e \u0432\u044b \u00ab\u0441\u0442\u0440\u0435\u043b\u044f\u0435\u0442\u0435 \u0432 \u0442\u0435\u043c\u043d\u043e\u0442\u0443\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u043e\u0444\u0438\u043b\u044c. \u0420\u0430\u0437\u0433\u043e\u043d, \u043f\u043b\u0430\u0442\u043e, \u0441\u043f\u0430\u0434; A\/B \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438; \u0444\u043e\u043d\u043e\u0432\u044b\u0435 \u0448\u0443\u043c\u043e\u0432\u044b\u0435 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0447\u0451\u0442. p50\/p90\/p99, Throughput, \u043e\u0448\u0438\u0431\u043a\u0438 \u043f\u043e \u043a\u043b\u0430\u0441\u0441\u0430\u043c (4xx\/5xx\/\u0442\u0430\u0439\u043c\u0430\u0443\u0442\u044b), \u043f\u0435\u0440-\u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438, SLA\/SLO.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u041d\u0435 \u043c\u0430\u0441\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u00ab\u043f\u0430\u0447\u043a\u0443 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432\u00bb \u043f\u043e\u0434 \u00ab\u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u043e\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a>\/<a href=\"https:\/\/k6.io\/\" rel=\"noopener noreferrer nofollow\">k6<\/a> \u0438\u043b\u0438 \u0445\u043e\u0442\u044f \u0431\u044b \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u0440\u0430\u043d\u043d\u0435\u0440 \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u043c \u0438 \u043c\u0435\u0442\u0440\u0438\u043a\u0430\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u0432\u0435\u0448\u0430\u0442\u044c <code>@pytest.mark.asyncio<\/code> \u043d\u0430 \u0443\u0442\u0438\u043b\u0438\u0442\u044b \u2014 \u0432\u044b\u043d\u043e\u0441\u0438\u0442\u044c \u0440\u0430\u043d\u043d\u0435\u0440 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043e\u0442 \u0442\u0435\u0441\u0442\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0431\u0438\u0440\u0430\u0442\u044c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043e\u0442\u0447\u0451\u0442\u044b; \u0431\u0435\u0437 \u044d\u0442\u043e\u0433\u043e \u0432\u044b\u0432\u043e\u0434\u044b \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b.<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0441\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u044c: \u0441\u0435\u0430\u043d\u0441\u044b, \u0434\u0430\u043d\u043d\u044b\u0435, \u00ab\u0434\u0443\u043c-\u0442\u0430\u0439\u043c\u044b\u00bb, \u0432\u0430\u0440\u0438\u0430\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 6. \u00ab40 \u0431\u0430\u0440\u0430\u0431\u0430\u043d\u043e\u0432 \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044b\u00bb<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412\u043e \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0435\u00bb \u0434\u0435\u0441\u044f\u0442\u043a\u0438 \u043e\u0434\u043d\u043e\u0442\u0438\u043f\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432: <code>press_down_arrow_key<\/code>, <code>press_up_arrow_key<\/code>, <code>press_left_arrow_key<\/code>, <code>press_right_arrow_key<\/code>, <code>press_enter_key<\/code>, <code>press_tab_key<\/code>, <code>press_backspace_key<\/code>, <code>press_delete_key<\/code>, <code>press_space_key<\/code>, <code>press_char_key<\/code>, <code>press_character_by_character<\/code>\u2026 \u0412\u0441\u0435 \u0434\u0435\u043b\u0430\u044e\u0442 \u043e\u0434\u043d\u043e \u0438 \u0442\u043e \u0436\u0435: \u0441\u0442\u0440\u043e\u044f\u0442 <code>ActionChains<\/code>, \u0436\u043c\u0443\u0442 \u043a\u043b\u0430\u0432\u0438\u0448\u0443 <code>n<\/code> \u0440\u0430\u0437 \u0438 \u0435\u0449\u0451 \u043f\u043e\u0434\u0441\u044b\u043f\u0430\u044e\u0442 <code>time.sleep(.1)<\/code> \u043c\u0435\u0436\u0434\u0443 \u043d\u0430\u0436\u0430\u0442\u0438\u044f\u043c\u0438.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">def press_down_arrow_key(driver, n):     action = ActionChains(driver)     for _ in range(n):         action.send_keys(Keys.ARROW_DOWN)         time.sleep(.1)     action.perform()  def press_enter_key(driver, n):     action = ActionChains(driver)     for _ in range(n):         action.send_keys(Keys.RETURN)         time.sleep(.1)     action.perform()  def press_character_by_character(driver, my_string: str):     action = ActionChains(driver)     for ch in my_string:         action.send_keys(ch)         time.sleep(.1)     action.perform() <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ol>\n<li>\n<p>\u0414\u0440\u0435\u0431\u0435\u0437\u0433 \u0438 flaky. \u0420\u0443\u0447\u043d\u044b\u0435 <code>sleep(.1)<\/code> \u2014 \u044d\u0442\u043e \u0433\u0430\u0434\u0430\u043d\u0438\u0435 \u043d\u0430 \u0442\u0430\u0439\u043c\u0438\u043d\u0433\u0430\u0445. \u041d\u0430 CI\/\u0434\u0440\u0443\u0433\u0438\u0445 \u043c\u0430\u0448\u0438\u043d\u0430\u0445 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0437\u043d\u044b\u043c.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435. \u0414\u0435\u0441\u044f\u0442\u043a\u0438 \u043f\u043e\u0447\u0442\u0438 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u2192 \u0442\u044f\u0436\u0435\u043b\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c\/\u043c\u0435\u043d\u044f\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u043f\u043e-\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438. \u0412 E2E \u043c\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041e\u043d \u043a\u043b\u0438\u043a\u0430\u0435\u0442 \u043f\u043e \u0432\u0438\u0434\u0438\u043c\u044b\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430\u043c; \u00ab\u0441\u0442\u0440\u0435\u043b\u043e\u0447\u043a\u0430\u043c\u0438 \u0432\u043d\u0438\u0437\u00bb \u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442 \u0440\u0435\u0434\u043a\u043e.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0436\u0434\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043d\u0438\u0437\u043a\u043e\u0443\u0440\u043e\u0432\u043d\u0435\u0432\u043e\u0441\u0442\u044c. \u041d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043b\u0430\u0432\u0438\u0448 \u2014 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044f\u044f \u043d\u0430\u0434\u0435\u0436\u0434\u0430, \u043a\u043e\u0433\u0434\u0430 \u043d\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0445 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u043e\u0432\/\u043c\u0435\u0442\u043e\u0434\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0439. \u041d\u0435\u0442 \u044f\u0432\u043d\u044b\u0445 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0439 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f (\u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f\/\u0441\u0442\u0430\u043d\u0435\u0442 \u043a\u043b\u0438\u043a\u0430\u0431\u0435\u043b\u044c\u043d\u044b\u043c), \u0442\u043e\u043b\u044c\u043a\u043e \u00ab\u0436\u043c\u0438 \u0438 \u043d\u0430\u0434\u0435\u0439\u0441\u044f\u00bb.<\/p>\n<\/li>\n<\/ol>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c2e\/233\/301\/c2e233301691e5223cf66224c0544826.png\" width=\"440\" height=\"800\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/c2e\/233\/301\/c2e233301691e5223cf66224c0544826.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c2e\/233\/301\/c2e233301691e5223cf66224c0544826.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e (Selenium)<\/h3>\n<h4>1) \u0423\u0431\u0440\u0430\u0442\u044c \u0437\u043e\u043e\u043f\u0430\u0440\u043a \u2014 \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043e\u0434\u0438\u043d \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u0445\u0435\u043b\u043f\u0435\u0440<\/h4>\n<pre><code class=\"python\">from selenium.webdriver import ActionChains  def press(driver, key, times=1, post_delay=0.0):     ac = ActionChains(driver)     for _ in range(times):         ac.send_keys(key)     ac.perform()     if post_delay:         time.sleep(post_delay) <\/code><\/pre>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435:<\/p>\n<pre><code class=\"python\">press(driver, Keys.ENTER) press(driver, Keys.ARROW_DOWN, times=3)<\/code><\/pre>\n<p>\u041d\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0435\u0437 \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044b \u043d\u0438\u043a\u0430\u043a (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0438\u0439 \u0441\u043f\u0438\u0441\u043e\u043a).<\/p>\n<h4>2) \u041f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0447\u0435\u0440\u0435\u0437 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u044b \u0438 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f<\/h4>\n<pre><code class=\"python\">from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC  def click_when_ready(driver, locator, timeout=10):     el = WebDriverWait(driver, timeout).until(         EC.element_to_be_clickable(locator)     )     el.click() <\/code><\/pre>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435:<\/p>\n<pre><code class=\"python\">click_when_ready(driver, (By.CSS_SELECTOR, '[data-test=\"submit\"]'))<\/code><\/pre>\n<h4>3) \u0414\u043b\u044f \u00ab\u043f\u0440\u043e\u043a\u0440\u0443\u0442\u043a\u0438\u00bb \u2014 \u0441\u043a\u0440\u043e\u043b\u043b \u043a \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443, \u0430 \u043d\u0435 \u00ab\u0441\u0442\u0440\u0435\u043b\u043a\u0438\u00bb<\/h4>\n<pre><code class=\"python\">def scroll_into_view(driver, element):     driver.execute_script(\"arguments[0].scrollIntoView({block:'center', inline:'center'})\", element)<\/code><\/pre>\n<p>\u0418 \u0437\u0430\u0442\u0435\u043c \u043a\u043b\u0438\u043a\/\u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u043f\u043e \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u0443 \u0441 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0435\u043c.<\/p>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e (Playwright \u2014 \u0435\u0449\u0451 \u043a\u043e\u0440\u043e\u0447\u0435 \u0438 \u043d\u0430\u0434\u0451\u0436\u043d\u0435\u0435)<\/h3>\n<p><a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a> \u0441\u0430\u043c \u0434\u0435\u043b\u0430\u0435\u0442 auto-wait \u0438 \u0443\u043c\u0435\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u043e\u0439 \u0442\u043e\u0447\u0435\u0447\u043d\u043e, \u0431\u0435\u0437 \u0440\u0443\u0447\u043d\u044b\u0445 <code>sleep<\/code>.<\/p>\n<pre><code class=\"python\"># \u043a\u043b\u0438\u043a \u043f\u043e \u043a\u043d\u043e\u043f\u043a\u0435 + \u0430\u0432\u0442\u043e\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f page.get_by_test_id(\"submit\").click()  # \u0441\u043a\u0440\u043e\u043b\u043b \u043a \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 \u043d\u0435 \u043d\u0443\u0436\u0435\u043d \u2014 \u043b\u043e\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u043c \u0434\u043e\u0436\u0434\u0451\u0442\u0441\u044f \u0438 \u0434\u043e\u0442\u044f\u043d\u0435\u0442\u0441\u044f page.locator(\"text=Next\").click()  # \u0435\u0441\u043b\u0438 \u0432\u0441\u0451 \u0436\u0435 \u043d\u0443\u0436\u043d\u0430 \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u0430 page.keyboard.press(\"ArrowDown\") page.keyboard.type(\"hello\")  # \u043f\u0435\u0447\u0430\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0438<\/code><\/pre>\n<h3>\u041a\u043e\u0433\u0434\u0430 \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u0430 \u0443\u043c\u0435\u0441\u0442\u043d\u0430?<\/h3>\n<ul>\n<li>\n<p>\u041d\u0430\u0442\u0438\u0432\u043d\u044b\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b\/\u043c\u0435\u043d\u044e, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441\u0442\u0440\u0435\u043b\u043a\u0430\u043c\u0438\/Tab \u0443 \u0436\u0438\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c (<a href=\"https:\/\/www.a11yproject.com\/\" rel=\"noopener noreferrer nofollow\">a11y<\/a>): \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u043f\u043e <code>Tab<\/code>\/<code>Shift+Tab<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0432\u043e\u0434 \u0432 \u043f\u043e\u043b\u044f \u0441 \u043c\u0430\u0441\u043a\u0430\u043c\u0438, \u0433\u0434\u0435 \u00ab\u0432\u043a\u043b\u0435\u0439\u043a\u0430\u00bb \u0444\u0430\u0439\u043b\u0430\u043c\u0438\/JS \u043d\u0435 \u043a\u0430\u0442\u0438\u0442.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u0430\u0436\u0435 \u0432 \u044d\u0442\u0438\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u043c\u0438\u043d\u0438\u043c\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0440\u0443\u0447\u043d\u044b\u0435 \u043f\u0430\u0443\u0437\u044b \u2014 \u043b\u0443\u0447\u0448\u0435 \u0434\u043e\u0436\u0434\u0430\u0442\u044c\u0441\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f (\u0444\u043e\u043a\u0443\u0441, \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u044c, enabled).<\/p>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u044b + \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f, \u043f\u043e\u0442\u043e\u043c \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u0430 \u043a\u0430\u043a \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0434\u0438\u043d \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 <code>press()<\/code> \u0432\u043c\u0435\u0441\u0442\u043e \u0434\u0435\u0441\u044f\u0442\u043a\u0430 \u043a\u043e\u043f\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u00ab\u043c\u0430\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0445\u00bb <code>sleep(.1)<\/code> \u0432\u043d\u0443\u0442\u0440\u0438 \u0445\u0435\u043b\u043f\u0435\u0440\u0430 \u2014 \u0436\u0434\u0451\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u2014 <a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a>: \u043c\u0435\u043d\u044c\u0448\u0435 \u043a\u043e\u0434\u0430, \u0431\u043e\u043b\u044c\u0448\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u044d\u043c\u0443\u043b\u0438\u0440\u0443\u0435\u043c \u00ab\u0441\u043a\u0440\u043e\u043b\u043b \u0441\u0442\u0440\u0435\u043b\u043a\u0430\u043c\u0438\u00bb \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0432 \u0432\u044c\u044e\u043f\u043e\u0440\u0442; \u0441\u043a\u0440\u043e\u043b\u043b\u0438\u043c \u043a \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 \u0438 \u043a\u043b\u0438\u043a\u0430\u0435\u043c.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 7. \u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0444\u0430\u0439\u043b\u0430 \u00ab\u0447\u0435\u0440\u0435\u0437 JS \u043f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c\u00bb<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412\u043c\u0435\u0441\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u0439 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u00bb \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u00ab\u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u00bb \u0441\u043a\u0440\u044b\u0442\u044b\u0439 <code>&lt;input type=\"file\"&gt;<\/code> \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0432\u043e\u0438\u0442\u044c \u0435\u043c\u0443 \u043f\u0443\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u0447\u0435\u0440\u0435\u0437 JS:<\/p>\n<pre><code class=\"python\">def upload_file_by_script(driver, input_xpath, file_path):     file_input = driver.find_element(By.XPATH, input_xpath)     driver.execute_script(\"arguments[0].style.display = 'block';\", file_input)     driver.execute_script(\"arguments[0].removeAttribute('disabled');\", file_input)     driver.execute_script(f\"arguments[0].value = '{file_path}';\", file_input)     return True<\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ol>\n<li>\n<p>\u0411\u0440\u0430\u0443\u0437\u0435\u0440\u043d\u0430\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c. \u0421\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u044b \u0437\u0430\u043f\u0440\u0435\u0449\u0430\u044e\u0442 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c <code>value<\/code> \u0443 <code>input[type=file]<\/code> \u0447\u0435\u0440\u0435\u0437 JS. \u042d\u0442\u043e \u0441\u043e\u0437\u043d\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438. \u0422\u0430\u043a\u043e\u0439 \u00ab\u0442\u0440\u044e\u043a\u00bb \u043b\u0438\u0431\u043e \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043b\u0438\u0431\u043e \u0441\u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u0436\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u043c\u0430\u0435\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u041d\u0430\u0441\u0438\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u0430\u0432\u043a\u0430 <code>display\/disabled<\/code> \u043c\u0435\u043d\u044f\u0435\u0442 DOM \u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432\u0438\u0434\u0436\u0435\u0442\u043e\u0432, \u0438\u0437-\u0437\u0430 \u0447\u0435\u0433\u043e \u043f\u0430\u0434\u0430\u044e\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438, \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438, \u0441\u0442\u0438\u043b\u0438. \u0422\u0435\u0441\u0442 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u043c\u0438\u0442\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<\/li>\n<li>\n<p>Flaky\/\u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c. \u0427\u0443\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 CSS\/\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u2014 \u0438 \u043c\u0430\u0433\u0438\u044f \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u043a\u0440\u043e\u0441\u0441-\u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043d\u043e\u0441\u0442\u0438. \u0422\u043e, \u0447\u0442\u043e \u00ab\u0437\u0430\u0432\u0435\u043b\u043e\u0441\u044c\u00bb \u0432 <a href=\"https:\/\/www.chromium.org\/chromium-projects\/\" rel=\"noopener noreferrer nofollow\">Chromium<\/a>, \u0447\u0430\u0441\u0442\u043e \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 <a href=\"https:\/\/www.firefox.com\/ru\/\" rel=\"noopener noreferrer nofollow\">Firefox<\/a>\/<a href=\"https:\/\/www.apple.com\/safari\/\" rel=\"noopener noreferrer nofollow\">Safari<\/a>.<\/p>\n<\/li>\n<\/ol>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/683\/eda\/d94\/683edad948477571a69c53f21951ae14.png\" width=\"1080\" height=\"1109\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/683\/eda\/d94\/683edad948477571a69c53f21951ae14.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/683\/eda\/d94\/683edad948477571a69c53f21951ae14.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e (Selenium)<\/h3>\n<h4>1) \u041a\u043b\u0430\u0441\u0441\u0438\u043a\u0430: send_keys() \u043d\u0430 input[type=file]<\/h4>\n<p><a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a> \u00ab\u0443\u043c\u0435\u0435\u0442\u00bb \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u2014 \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u044b\u0439 \u043f\u0443\u0442\u044c:<\/p>\n<pre><code class=\"python\">from selenium.webdriver.common.by import By from pathlib import Path  def upload_file(driver, input_locator: tuple[str, str], path: str):     abs_path = str(Path(path).resolve())     elem = driver.find_element(*input_locator)     elem.send_keys(abs_path)  # &lt;-- \u0432\u043e\u0442 \u0438 \u0432\u0441\u0451<\/code><\/pre>\n<blockquote>\n<p>\u041f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435: \u0435\u0441\u043b\u0438 \u0438\u043d\u043f\u0443\u0442 disabled \u0438\u043b\u0438 \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d, \u043d\u0430\u0434\u043e \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443\/label, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0434\u0438\u0430\u043b\u043e\u0433 \u2014 \u043d\u043e \u0441\u0430\u043c \u0444\u0430\u0439\u043b \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u043c \u0447\u0435\u0440\u0435\u0437 <code>send_keys<\/code> \u043f\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 input, \u0430 \u043d\u0435 \u0447\u0435\u0440\u0435\u0437 JS.<\/p>\n<\/blockquote>\n<h4>2) \u0421\u043a\u0440\u044b\u0442\u044b\u0439  (aria-\u0432\u0438\u0434\u0436\u0435\u0442\u044b)<\/h4>\n<p>\u0418\u043d\u043e\u0433\u0434\u0430 <code>&lt;input type=\"file\"&gt;<\/code> \u0441\u043a\u0440\u044b\u0442, \u0430 UI \u2014 \u044d\u0442\u043e \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430\/\u043b\u0435\u0439\u0431\u043b. \u0414\u0435\u043b\u0430\u0439\u0442\u0435 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"python\">from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC  def upload_via_label_then_set(driver, button_locator, input_locator, path):     WebDriverWait(driver, 10).until(EC.element_to_be_clickable(button_locator)).click()     # \u041f\u043e\u0441\u043b\u0435 \u043a\u043b\u0438\u043a\u0430 framework \u0447\u0430\u0441\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 input \u00ab\u0436\u0438\u0432\u044b\u043c\u00bb     upload_file(driver, input_locator, path)<\/code><\/pre>\n<p>\u041d\u0435 \u0442\u0440\u043e\u0433\u0430\u0439\u0442\u0435 <code>display\/disabled<\/code> \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u2014 \u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e \u0441\u0430\u043c\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 input \u0432 \u00ab\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u00bb.<\/p>\n<h4>3) Selenium Grid \/ \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0434\u0440\u0430\u0439\u0432\u0435\u0440<\/h4>\n<p>\u041d\u0430 \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u043c \u0434\u0440\u0430\u0439\u0432\u0435\u0440\u0435 \u043d\u0443\u0436\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c FileDetector, \u0438\u043d\u0430\u0447\u0435 \u043f\u0443\u0442\u044c \u0431\u0443\u0434\u0435\u0442 \u00ab\u043d\u0430 \u0432\u0430\u0448\u0435\u0439 \u043c\u0430\u0448\u0438\u043d\u0435\u00bb, \u0430 \u043d\u0435 \u043d\u0430 \u043d\u043e\u0434\u0435:<\/p>\n<pre><code class=\"python\">from selenium.webdriver.remote.file_detector import LocalFileDetector driver.file_detector = LocalFileDetector()  elem = driver.find_element(By.CSS_SELECTOR, 'input[type=\"file\"]') elem.send_keys(str(Path(\"files\/doc.pdf\").resolve()))<\/code><\/pre>\n<h4>4) Drag&amp;Drop-\u0432\u0438\u0434\u0436\u0435\u0442\u044b (dropzone)<\/h4>\n<p>\u0415\u0441\u043b\u0438 \u0444\u0440\u043e\u043d\u0442 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0444\u0430\u0439\u043b \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u00ab\u043f\u0435\u0440\u0435\u0442\u0430\u0441\u043a\u0438\u0432\u0430\u043d\u0438\u0435\u00bb, \u0443 \u0432\u0430\u0441 \u0434\u0432\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430:<\/p>\n<ul>\n<li>\n<p>\u041e\u0431\u043e\u0439\u0442\u0438 UI \u0438 \u0431\u0438\u0442\u044c \u0432 API \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e (\u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0442\u0435\u0441\u0442\u043e\u0432).<\/p>\n<\/li>\n<li>\n<p>\u0418\u043b\u0438 \u0441\u044b\u043c\u0438\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c drop-\u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u0412 Selenium \u044d\u0442\u043e \u0433\u0440\u043e\u043c\u043e\u0437\u0434\u043a\u043e. \u0427\u0435\u0441\u0442\u043d\u0435\u0435 \u0437\u0434\u0435\u0441\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a>.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e (Playwright \u2014 \u043f\u0440\u043e\u0449\u0435 \u0438 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0435\u0435)<\/h3>\n<p><a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a> \u0440\u0435\u0448\u0430\u0435\u0442 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u00ab\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438\u00bb \u0438 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0434\u0435\u043b\u0430\u0442\u044c \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0432\u0438\u0434\u0438\u043c\u044b\u043c:<\/p>\n<pre><code class=\"python\"># Python page.set_input_files('input[type=\"file\"]', 'files\/doc.pdf')  # \u0435\u0441\u043b\u0438 \u0435\u0441\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430\/\u043b\u0435\u0439\u0431\u043b page.get_by_role(\"button\", name=\"Upload\").click() page.set_input_files('input[type=\"file\"]', 'files\/doc.pdf')  # \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u0444\u0430\u0439\u043b\u044b page.set_input_files('input[type=\"file\"]', ['a.png', 'b.png'])<\/code><\/pre>\n<p>\u0414\u043b\u044f dropzone-\u0432\u0438\u0434\u0436\u0435\u0442\u043e\u0432 \u0447\u0430\u0441\u0442\u043e \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 input \u043f\u043e \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u0443 \u2014 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0438 \u0434\u0435\u0440\u0436\u0430\u0442 \u0435\u0433\u043e \u0432 DOM. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u2014 <a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a> \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 <code>page.dispatch_event('selector', 'drop', data)<\/code> \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 API-\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443.<\/p>\n<h3>\u0427\u0430\u0441\u0442\u044b\u0435 \u0433\u0440\u0430\u0431\u043b\u0438 \u0438 \u043a\u0430\u043a \u0438\u0445 \u043e\u0431\u043e\u0439\u0442\u0438<\/h3>\n<ul>\n<li>\n<p>\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0443\u0442\u0438. \u0412\u0441\u0435\u0433\u0434\u0430 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u0435 \u043f\u0443\u0442\u044c \u043a \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u043e\u043c\u0443.<\/p>\n<\/li>\n<li>\n<p>Iframe. \u0415\u0441\u043b\u0438 input \u0432\u043d\u0443\u0442\u0440\u0438 iframe \u2014 \u0441\u043d\u0430\u0447\u0430\u043b\u0430 <code>frame = page.frame(name=\"...\")<\/code> \/ <code>driver.switch_to.frame(...)<\/code>, \u043f\u043e\u0442\u043e\u043c \u2014 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430.<\/p>\n<\/li>\n<li>\n<p>\u041c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 input. \u0414\u043b\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0444\u0430\u0439\u043b\u043e\u0432 \u043d\u0443\u0436\u0435\u043d \u0430\u0442\u0440\u0438\u0431\u0443\u0442 <code>multiple<\/code> \u0443 input; \u0438\u043d\u0430\u0447\u0435 \u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443.<\/p>\n<\/li>\n<li>\n<p>\u0410\u043d\u0442\u0438\u0432\u0438\u0440\u0443\u0441\/\u0441\u0435\u0442\u0438. \u041d\u0430 CI \u043f\u0443\u0442\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u043d\u0430 \u0430\u0433\u0435\u043d\u0442\u0435, \u0430 \u043d\u0435 \u043d\u0430 \u0432\u0430\u0448\u0435\u0439 \u043c\u0430\u0448\u0438\u043d\u0435. \u041f\u043e\u0434\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0439\u0442\u0435 \u0444\u0430\u0439\u043b\u044b \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439\/\u0430\u0440\u0442\u0435\u0444\u0430\u043a\u0442\u044b job\u2019\u0430.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0444\u0440\u043e\u043d\u0442\u0430. \u041f\u043e\u0441\u043b\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0439\u0442\u0435 UI-\u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435: \u043f\u0440\u0435\u0432\u044c\u044e, \u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430, \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441, \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0441\u0442\u0430\u0442\u0443\u0441, \u0430 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u043a\u0442 \u00ab\u043e\u0442\u0434\u0430\u043b send_keys\u00bb.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u0444\u0430\u0439\u043b\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 <code>send_keys<\/code> (<a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a>) \u0438\u043b\u0438 <code>set_input_files<\/code> (Playwright).<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0445 \u0440\u0430\u043d\u043d\u0435\u0440\u043e\u0432 \u2014 <code>LocalFileDetector<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041a\u043b\u0438\u043a\u0430\u0435\u043c \u043f\u043e \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0430\u043c (\u043a\u043d\u043e\u043f\u043a\u0430\/label), \u043d\u0435 \u043b\u043e\u043c\u0430\u044f DOM.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0438 dropzone \u2014 \u043b\u0438\u0431\u043e API, \u043b\u0438\u0431\u043e Playwright\/\u0441\u043b\u043e\u0436\u043d\u044b\u0439 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439 drop-\u0441\u043e\u0431\u044b\u0442\u0438\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u043d\u0430\u0437\u043d\u0430\u0447\u0430\u0435\u043c <code>value<\/code> \u0443 file-input \u0447\u0435\u0440\u0435\u0437 JS.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u043c\u0435\u043d\u044f\u0435\u043c \u0441\u0442\u0438\u043b\u0438\/disabled \u0443 input \u0434\u043b\u044f \u00ab\u043e\u0431\u0445\u043e\u0434\u0430\u00bb \u2014 \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 \u0442\u0435\u0441\u0442 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u044b\u043c \u0438 \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 8. \u0424\u0430\u043b\u044c\u0448\u0438\u0432\u044b\u0435 HTTP-\u043e\u0442\u0432\u0435\u0442\u044b \u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u044b 0\/310\/520<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412 \u00ab\u043e\u0431\u0451\u0440\u0442\u043a\u0435\u00bb \u043d\u0430\u0434 <code>requests<\/code>\/<code>httpx<\/code> \u043b\u043e\u0432\u044f\u0442\u0441\u044f \u043b\u044e\u0431\u044b\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u0441\u043b\u0435 \u0447\u0435\u0433\u043e \u0440\u0443\u043a\u0430\u043c\u0438 \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f \u00ab\u0441\u0438\u043d\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439\u00bb <code>Response()<\/code> \u0441 \u043f\u0440\u0438\u0434\u0443\u043c\u0430\u043d\u043d\u044b\u043c <code>status_code<\/code> \u2014 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 <code>0<\/code> (\u043d\u0435\u0442 \u0441\u0435\u0442\u0438), <code>310<\/code> (\u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0440\u0435\u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0432), <code>520<\/code> (\u00ab\u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430\u00bb). \u0421\u043d\u0430\u0440\u0443\u0436\u0438 \u0432\u0441\u0451 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043a\u0430\u043a \u00ab\u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439\u00bb HTTP-\u043e\u0442\u0432\u0435\u0442.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">import requests  def send_get_full_request(url, **kwargs) -&gt; requests.Response:     try:         return requests.get(url, **kwargs)  # \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 (200\/4xx\/5xx)     except requests.exceptions.RequestException as e:         # \"\u0421\u0438\u043d\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439\" \u043e\u0442\u0432\u0435\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f         resp = requests.Response()         resp.url = url         resp._content = str(e).encode(\"utf-8\")         resp.reason = type(e).__name__         if isinstance(e, requests.exceptions.Timeout):             resp.status_code = 408         elif isinstance(e, requests.exceptions.ConnectionError):             resp.status_code = 0       # \ud83d\udc4e \u043d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043a\u043e\u0434         elif isinstance(e, requests.exceptions.TooManyRedirects):             resp.status_code = 310     # \ud83d\udc4e \u043d\u0435\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 \u043a\u043e\u0434         else:             resp.status_code = 520     # \ud83d\udc4e \u00ab\u043b\u0435\u0432\u044b\u0439\u00bb \u043a\u043e\u0434         return resp <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>\u041f\u043e\u0434\u043c\u0435\u043d\u0430 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0438. <code>0<\/code> \u2014 \u043d\u0435 HTTP-\u043a\u043e\u0434 \u0432\u043e\u043e\u0431\u0449\u0435; <code>310<\/code>\/<code>520<\/code> \u2014 \u043d\u0435\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435. \u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433, \u0440\u0435\u0442\u0440\u0430\u0438, SLA\/\u0430\u043b\u0451\u0440\u0442\u044b, \u043b\u0438\u0431\u044b-\u043a\u043b\u0438\u0435\u043d\u0442\u044b \u0438 middleware \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u044e\u0442 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u043e\u0442\u043b\u0438\u0447\u0430\u0442\u044c <em>\u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f<\/em> \u043e\u0442 <em>\u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 HTTP-\u043e\u0442\u0432\u0435\u0442\u043e\u0432 \u0441\u0435\u0440\u0432\u0435\u0440\u0430<\/em>.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0436\u043d\u0430\u044f \u0442\u0435\u043b\u0435\u043c\u0435\u0442\u0440\u0438\u044f. \u041c\u0435\u0442\u0440\u0438\u043a\u0438 \u00ab\u043e\u0448\u0438\u0431\u043e\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 5xx\u00bb \u0432\u0434\u0440\u0443\u0433 \u0440\u0430\u0441\u0442\u0443\u0442 \u0438\u0437-\u0437\u0430 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u043e\u0432 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0435 \u2014 \u0432\u044b \u043b\u0435\u0447\u0438\u0442\u0435 \u043d\u0435 \u0442\u0443 \u0441\u0442\u043e\u0440\u043e\u043d\u0443.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u043e\u0448\u0438\u0431\u043e\u043a. \u041a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 <code>raise_for_status()<\/code>\/\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439, \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u00ab\u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0439 \u0432\u044b\u0437\u043e\u0432 \u0441 \u043a\u043e\u0434\u043e\u043c 0\/520\u00bb \u0438 \u0438\u0434\u0451\u0442 \u0434\u0430\u043b\u044c\u0448\u0435.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0430 \u0432 \u043d\u0438\u043a\u0443\u0434\u0430. \u041f\u043e\u0442\u0435\u0440\u044f\u043d \u0441\u0442\u0435\u043a \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043d\u0435 \u0432\u0438\u0434\u043d\u043e, \u0433\u0434\u0435 \u0438\u043c\u0435\u043d\u043d\u043e \u0443\u043f\u0430\u043b\u0438 DNS\/SSL\/\u043a\u043e\u043d\u043d\u0435\u043a\u0442\/\u0442\u0430\u0439\u043c\u0430\u0443\u0442.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u044c. \u0420\u0435\u0442\u0440\u0430\u0438 \u043f\u043e \u00ab\u043a\u043e\u0434\u0430\u043c 0\/520\u00bb \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0441 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u043c\u0438 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430\u043c\u0438 (\u043e\u043d\u0438 \u0436\u0434\u0443\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439, \u0430 \u043d\u0435 \u0432\u044b\u0434\u0443\u043c\u0430\u043d\u043d\u044b\u0445 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u0432).<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/990\/bbc\/a10\/990bbca10670a37fd4f9cfeff87f9363.png\" width=\"746\" height=\"357\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/990\/bbc\/a10\/990bbca10670a37fd4f9cfeff87f9363.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/990\/bbc\/a10\/990bbca10670a37fd4f9cfeff87f9363.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 A. \u00ab\u0427\u0438\u0441\u0442\u044b\u0439\u00bb \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442: \u043b\u0438\u0431\u043e \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 Response, \u043b\u0438\u0431\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h4>\n<ul>\n<li>\n<p>\u041d\u0430 <em>HTTP-\u0443\u0440\u043e\u0432\u043d\u0435<\/em> \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 (200\/3xx\/4xx\/5xx) \u0438 \u043f\u0440\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u043c <code>raise_for_status()<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430 <em>\u0441\u0435\u0442\u0435\u0432\u043e\u043c \u0443\u0440\u043e\u0432\u043d\u0435<\/em> (\u0442\u0430\u0439\u043c\u0430\u0443\u0442\/\u043a\u043e\u043d\u043d\u0435\u043a\u0442\/DNS\/SSL) \u2014 \u043d\u0435 \u043f\u0440\u044f\u0447\u0435\u043c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443: \u043f\u0440\u043e\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0435\u043c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u0440\u0443\u0436\u0443. \u0420\u0435\u0442\u0440\u0430\u0438\u043c \u043f\u043e \u0442\u0438\u043f\u0430\u043c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439.<\/p>\n<\/li>\n<\/ul>\n<pre><code class=\"python\">import httpx from backoff import on_exception, expo  # pip install backoff  class Api:     def __init__(self, *, timeout: float = 5.0):         self.client = httpx.Client(timeout=timeout)      @on_exception(expo, (httpx.ConnectError, httpx.ReadTimeout), max_tries=3)     def get(self, url: str, **kwargs) -&gt; httpx.Response:         resp = self.client.get(url, **kwargs)         return resp  # real HTTP response; caller decides to raise_for_status()      def close(self):         self.client.close()  # \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 api = Api(timeout=5) try:     r = api.get(\"https:\/\/api.example.com\/items\")     r.raise_for_status()             # HTTP-\u043e\u0448\u0438\u0431\u043a\u0438 \u2014 \u043a\u0430\u043a HTTP-\u043e\u0448\u0438\u0431\u043a\u0438     data = r.json() except (httpx.ConnectError, httpx.ReadTimeout) as e:     # \u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f: \u043c\u043e\u0436\u043d\u043e \u0440\u0435\u0442\u0440\u0430\u0438\u0442\u044c\/\u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e     ... except httpx.HTTPStatusError as e:     # \u0421\u0435\u0440\u0432\u0435\u0440 \u043e\u0442\u0432\u0435\u0442\u0438\u043b 4xx\/5xx \u2014 \u044d\u0442\u043e \u0443\u0436\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438     ... <\/code><\/pre>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 B. \u0415\u0441\u043b\u0438 \u043d\u0443\u0436\u0435\u043d \u00ab\u043e\u0431\u043e\u0431\u0449\u0451\u043d\u043d\u044b\u0439\u00bb \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u2014 \u0434\u0435\u043b\u0430\u0435\u043c \u044f\u0432\u043d\u0443\u044e \u043c\u043e\u0434\u0435\u043b\u044c<\/h4>\n<pre><code class=\"python\">from dataclasses import dataclass from typing import Any, Optional import httpx  @dataclass class ApiResult:     ok: bool     status: Optional[int]          # None \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043e\u0448\u0438\u0431\u043e\u043a     data: Optional[Any]     error: Optional[Exception]  def safe_get(client: httpx.Client, url: str) -&gt; ApiResult:     try:         resp = client.get(url)         # \u043d\u0435 \u0441\u043a\u0440\u044b\u0432\u0430\u0435\u043c 4xx\/5xx: \u044d\u0442\u043e \u0432\u0441\u0451 \u0435\u0449\u0451 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 HTTP-\u043e\u0442\u0432\u0435\u0442         return ApiResult(ok=resp.is_success, status=resp.status_code,                          data=resp.json() if resp.headers.get(\"content-type\",\"\").startswith(\"application\/json\") else resp.text,                          error=None)     except httpx.RequestError as e:         # \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u2192 status=None, \u043e\u0448\u0438\u0431\u043a\u0430 \u044f\u0432\u043d\u0430\u044f         return ApiResult(ok=False, status=None, data=None, error=e) <\/code><\/pre>\n<p>\u0422\u0430\u043a \u0442\u0435\u0441\u0442\u0430\u043c \u0438 \u043f\u0440\u043e\u0434-\u043a\u043e\u0434\u0443 \u043f\u043e\u043d\u044f\u0442\u043d\u043e, \u0447\u0442\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u0441\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c: HTTP-\u043e\u0448\u0438\u0431\u043a\u0430 \u0438\u043b\u0438 \u0441\u0435\u0442\u044c.<\/p>\n<h3>\u0412\u0430\u0436\u043d\u044b\u0435 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438!<\/h3>\n<ul>\n<li>\n<p>\u0422\u0430\u0439\u043c\u0430\u0443\u0442\u044b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e. \u041d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0434\u0435\u043b\u0430\u0439\u0442\u0435 \u00ab\u0432\u0435\u0447\u043d\u044b\u0445\u00bb \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u0420\u0435\u0442\u0440\u0430\u0438 \u043f\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c, \u0430 \u043d\u0435 \u043f\u043e \u00ab\u043a\u043e\u0434\u0430\u043c 0\/520\u00bb. \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0439\u0442\u0435 jitter\/backoff.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0433\u0438\u0440\u0443\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u043f\u043e\u043b\u044f\u043c\u0438: \u043c\u0435\u0442\u043e\u0434, URL, <code>status_code<\/code> (\u0435\u0441\u043b\u0438 \u0435\u0441\u0442\u044c), \u0442\u0438\u043f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u0435\u0441\u043b\u0438 \u0435\u0441\u0442\u044c), \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c, \u043f\u043e\u043f\u044b\u0442\u043a\u0443.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u0433\u043b\u043e\u0442\u0430\u0439\u0442\u0435 \u0441\u0442\u0435\u043a. \u0412 \u043b\u043e\u0433\u0430\u0445 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c\u0441\u044f traceback \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041a\u043e\u043d\u0442\u0435\u043d\u0442-\u0442\u0430\u0439\u043f\/\u043f\u0430\u0440\u0441\u0438\u043d\u0433. \u041d\u0435 \u0437\u043e\u0432\u0438\u0442\u0435 \u0431\u0435\u0437\u0434\u0443\u043c\u043d\u043e <code>.json()<\/code>; \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0439\u0442\u0435 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0438\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 <code>.is_success<\/code> \u0438 fallback \u043a <code>text<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041c\u0435\u0442\u0440\u0438\u043a\u0438. \u0420\u0430\u0437\u0432\u043e\u0434\u0438\u0442\u0435 \u00abHTTP-\u043e\u0448\u0438\u0431\u043a\u0438\u00bb (4xx\/5xx) \u0438 \u00ab\u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u00bb (timeout\/connect). \u042d\u0442\u043e \u0440\u0430\u0437\u043d\u044b\u0435 SLO \u0438 \u0440\u0430\u0437\u043d\u044b\u0435 \u0432\u043b\u0430\u0434\u0435\u043b\u044c\u0446\u044b.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u041d\u0435 \u043f\u043e\u0434\u043c\u0435\u043d\u044f\u0435\u043c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043d\u0430 \u00ab\u043e\u0442\u0432\u0435\u0442\u044b\u00bb \u0441 \u0444\u0430\u043b\u044c\u0448-\u043a\u043e\u0434\u0430\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 <code>Response<\/code>; \u0441\u0435\u0442\u0435\u0432\u044b\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u2014 \u043a\u0430\u043a \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.<\/p>\n<\/li>\n<li>\n<p>\u0420\u0435\u0442\u0440\u0430\u0438\u043c \u043f\u043e <code>ConnectError<\/code>\/<code>Timeout<\/code> (backoff + jitter).<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438\/\u043b\u043e\u0433\u0438 \u0434\u043b\u044f HTTP-\u043e\u0448\u0438\u0431\u043e\u043a \u0438 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p><code>json=<\/code> \u0432\u043c\u0435\u0441\u0442\u043e \u0440\u0443\u0447\u043d\u043e\u0433\u043e <code>json.dumps<\/code>; <code>raise_for_status()<\/code> \u0442\u0430\u043c, \u0433\u0434\u0435 \u043d\u0443\u0436\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p>\u0415\u0441\u043b\u0438 \u0445\u043e\u0447\u0435\u0442\u0441\u044f \u00ab\u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u00bb \u2014 \u0434\u0435\u043b\u0430\u0435\u043c <strong>\u044f\u0432\u043d\u0443\u044e<\/strong> \u043c\u043e\u0434\u0435\u043b\u044c, \u0430 \u043d\u0435 \u043f\u0440\u0438\u0434\u0443\u043c\u044b\u0432\u0430\u0435\u043c HTTP-\u043a\u043e\u0434\u044b.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 9. \u00ab\u041e\u0434\u0438\u043d \u0444\u0430\u0439\u043b \u043d\u0430 3500 \u0441\u0442\u0440\u043e\u043a \u2014 \u044d\u0442\u043e \u043d\u0435 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u00bb<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412\u0441\u044f \u043b\u043e\u0433\u0438\u043a\u0430 \u2014 \u043e\u0442 UI \u0438 API \u0434\u043e SQL, VPN \u0438 \u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u2014 \u0441\u043e\u0431\u0440\u0430\u043d\u0430 \u0432 \u043e\u0434\u0438\u043d \u0433\u0438\u0433\u0430\u043d\u0442\u0441\u043a\u0438\u0439 \u0444\u0430\u0439\u043b \u043d\u0430 3500 \u0441\u0442\u0440\u043e\u043a. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439, \u043d\u0438\u043a\u0430\u043a\u043e\u0439 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b, \u043f\u0440\u043e\u0441\u0442\u043e \u00ab\u043a\u0443\u0447\u0430 \u0432\u0441\u0435\u0433\u043e\u00bb.<\/p>\n<p>\u0417\u043e\u043b\u043e\u0442\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0430 \u043d\u0430 \u0442\u0430\u043a\u043e\u043c \u043a\u043e\u0434\u0435 \u043c\u043e\u0433\u043b\u0430 \u0431\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u043e\u0439:<\/p>\n<blockquote>\n<p>\u00ab\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u2014 \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0439. \u0421\u043b\u043e\u043c\u0430\u043b\u043e\u0441\u044c \u2014 \u043d\u0435 \u043f\u043e\u0447\u0438\u043d\u0438\u0448\u044c\u00bb.<\/p>\n<\/blockquote>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0443\u043f\u0440\u043e\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\"># \u0412 \u043e\u0434\u043d\u043e\u043c \u0438 \u0442\u043e\u043c \u0436\u0435 \u0444\u0430\u0439\u043b\u0435 lib.py:  class LibUI:     def click_element_by_xpath(...): ...     def press_down_arrow(...): ...     # \u0434\u0435\u0441\u044f\u0442\u043a\u0438 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0434\u043b\u044f \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044b  class LibSQL:     def connect_postgres(...): ...     def connect_mysql(...): ...     def execute_query(...): ...  class LibAPI:     def send_post_request(...): ...     def send_get_request(...): ...     # \u0432\u043d\u0443\u0442\u0440\u0438 \u0435\u0449\u0451 exec \u0438 \u00ab\u0444\u0430\u043b\u044c\u0448\u0438\u0432\u044b\u0435 \u043a\u043e\u0434\u044b\u00bb  # \u0435\u0449\u0451 \u043d\u0438\u0436\u0435: \u0443\u0442\u0438\u043b\u0438\u0442\u044b \u0434\u043b\u044f VPN, \u0444\u0430\u0439\u043b\u043e\u0432\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b, \u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u0447\u0435\u0440\u0435\u0437 httpx, CSV-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438... <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>\u041d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 SRP (<a href=\"https:\/\/en.wikipedia.org\/wiki\/Single-responsibility_principle\" rel=\"noopener noreferrer nofollow\">Single Responsibility Principle<\/a>). \u041e\u0434\u0438\u043d \u0444\u0430\u0439\u043b \u0434\u0435\u043b\u0430\u0435\u0442 \u0432\u0441\u0451 \u0441\u0440\u0430\u0437\u0443. UI \u2260 API \u2260 SQL \u2260 DevOps. \u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u043c\u043e\u0434\u0443\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u041d\u0435\u043b\u044c\u0437\u044f \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u0443\u0441\u043e\u043a \u043a\u043e\u0434\u0430 \u0432 \u0434\u0440\u0443\u0433\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435: \u043e\u043d \u0442\u044f\u043d\u0435\u0442 \u0437\u0430 \u0441\u043e\u0431\u043e\u0439 \u0432\u0435\u0441\u044c \u00ab\u0437\u043e\u043e\u043f\u0430\u0440\u043a\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u0413\u0438\u0433\u0430\u043d\u0442\u0441\u043a\u0438\u0439 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0434\u043e\u043b\u0433. \u041b\u044e\u0431\u0430\u044f \u043f\u0440\u0430\u0432\u043a\u0430\/\u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433 \u2192 \u0440\u0438\u0441\u043a \u043f\u043e\u043b\u043e\u043c\u0430\u0442\u044c \u0447\u0443\u0436\u0443\u044e \u0447\u0430\u0441\u0442\u044c, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0442\u0435\u0441\u0442\u044b \u0437\u0430\u0432\u044f\u0437\u0430\u043d\u044b \u043d\u0430 \u0432\u0435\u0441\u044c \u043a\u043e\u043c\u0431\u0430\u0439\u043d.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u0440\u043e\u0433 \u0432\u0445\u043e\u0434\u0430. \u041d\u043e\u0432\u0438\u0447\u043e\u043a \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0444\u0430\u0439\u043b \u0438 \u0442\u0435\u0440\u044f\u0435\u0442\u0441\u044f. \u0413\u0434\u0435 UI? \u0413\u0434\u0435 \u0431\u0430\u0437\u0430? \u0413\u0434\u0435 API? \u0412\u0441\u0451 \u0432 \u043e\u0434\u043d\u043e\u0439 \u043f\u0440\u043e\u0441\u0442\u044b\u043d\u0435.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0441\u0442\u0438. \u0422\u0430\u043a\u043e\u0439 \u043c\u043e\u043d\u043e\u043b\u0438\u0442 \u043d\u0435\u043b\u044c\u0437\u044f \u0438\u0437\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e \u043f\u043e\u043a\u0440\u044b\u0442\u044c \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u0430\u043c\u0438. \u0412\u0441\u0451 \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0447\u0435\u0440\u0435\u0437 \u0433\u043b\u043e\u0431\u0430\u043b\u044b.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0414\u0435\u043b\u0438\u043c \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043c\u043e\u0434\u0443\u043b\u0438<\/h4>\n<ul>\n<li>\n<p><code>ui.py<\/code> \u2014 \u043e\u0431\u0451\u0440\u0442\u043a\u0438 \u043d\u0430\u0434 <a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a>\/<a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a>.<\/p>\n<\/li>\n<li>\n<p><code>api.py<\/code> \u2014 \u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a>.<\/p>\n<\/li>\n<li>\n<p><code>db.py<\/code> \u2014 <a href=\"https:\/\/www.sqlalchemy.org\/\" rel=\"noopener noreferrer nofollow\">SQLAlchemy<\/a>\/<a href=\"https:\/\/www.psycopg.org\/docs\/\" rel=\"noopener noreferrer nofollow\">psycopg2<\/a> \u0443\u0442\u0438\u043b\u0438\u0442\u044b.<\/p>\n<\/li>\n<li>\n<p><code>load.py<\/code> \u2014 \u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438 \u0432 <a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a>.<\/p>\n<\/li>\n<li>\n<p><code>tools\/<\/code> \u2014 \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 (\u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u043f\u0430\u0440\u0441\u0438\u043d\u0433).<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u0441\u0432\u043e\u0451.<\/p>\n<h4>\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430\u00bb \u043a\u0430\u043a \u043f\u0430\u043a\u0435\u0442<\/h4>\n<pre><code>my_framework\/     __init__.py     ui.py     api.py     db.py     load.py     utils\/         logging.py         files.py<\/code><\/pre>\n<h4>\u041f\u0440\u0438\u043c\u0435\u0440<\/h4>\n<pre><code class=\"python\"># ui.py from playwright.sync_api import Page  def click(page: Page, selector: str):     page.locator(selector).click()       # api.py import httpx  def get(client: httpx.Client, url: str):     return client.get(url) <\/code><\/pre>\n<p>\u2192 \u0422\u0435\u0441\u0442\u0430\u043c \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u00ab\u0432\u0441\u0451 \u043f\u043e\u0434\u0440\u044f\u0434\u00bb. \u041e\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0443\u0436\u043d\u043e\u0435.<\/p>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/h3>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0451 \u0432 \u043e\u0434\u0438\u043d \u00ab\u0431\u043e\u0433-\u0444\u0430\u0439\u043b\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0435\u043b\u0438\u0442\u044c \u043a\u043e\u0434 \u043d\u0430 \u043c\u043e\u0434\u0443\u043b\u0438: UI, API, DB, \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430.<\/p>\n<\/li>\n<li>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 (<a href=\"https:\/\/playwright.dev\/python\/\" rel=\"noopener noreferrer nofollow\">Playwright<\/a>, <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a>, <a href=\"https:\/\/www.sqlalchemy.org\/\" rel=\"noopener noreferrer nofollow\">SQLAlchemy<\/a>, <a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a>).<\/p>\n<\/li>\n<li>\n<p>\u0421\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/en.wikipedia.org\/wiki\/Single-responsibility_principle\" rel=\"noopener noreferrer nofollow\">SRP<\/a>: \u043e\u0434\u0438\u043d \u043c\u043e\u0434\u0443\u043b\u044c = \u043e\u0434\u043d\u0430 \u0437\u043e\u043d\u0430 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0438\u0441\u0430\u0442\u044c \u044e\u043d\u0438\u0442-\u0442\u0435\u0441\u0442\u044b \u043d\u0430 \u0443\u0442\u0438\u043b\u0438\u0442\u044b, \u0430 \u043d\u0435 \u043d\u0430 \u00ab\u043a\u043e\u043c\u0431\u0430\u0439\u043d\u00bb.<\/p>\n<\/li>\n<\/ul>\n<h2>\u041f\u043e\u0447\u0435\u043c\u0443 \u044d\u0442\u043e \u0432\u0440\u0435\u0434\u043d\u043e?<\/h2>\n<p>\u041d\u0430 \u043f\u0435\u0440\u0432\u044b\u0439 \u0432\u0437\u0433\u043b\u044f\u0434 \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0435 \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0438\u00bb \u043a\u0430\u0436\u0443\u0442\u0441\u044f \u043f\u0440\u043e\u0441\u0442\u044b\u043c\u0438 \u0438 \u0443\u0434\u043e\u0431\u043d\u044b\u043c\u0438: \u0432\u044b\u0437\u0432\u0430\u043b \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u043e\u0434 <code>LibUI.click_element_by_xpath(...)<\/code> \u2014 \u0438 \u0442\u0435\u0441\u0442 \u0433\u043e\u0442\u043e\u0432. \u041d\u043e \u044d\u0442\u043e \u0438\u043b\u043b\u044e\u0437\u0438\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u044b, \u0437\u0430 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043f\u043e\u0442\u043e\u043c \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043e\u0447\u0435\u043d\u044c \u0434\u043e\u0440\u043e\u0433\u043e \u043f\u043b\u0430\u0442\u0438\u0442\u044c.<\/p>\n<h2>\u042d\u0444\u0444\u0435\u043a\u0442 \u0414\u0430\u043d\u043d\u0438\u043d\u0433\u0430\u2013\u041a\u0440\u044e\u0433\u0435\u0440\u0430 \u0432 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0438<\/h2>\n<p>\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u0430\u0432\u0442\u043e\u0440\u044b \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u0439 \u0441\u0430\u043c\u0438 \u043d\u0435 \u043e\u0441\u043e\u0437\u043d\u0430\u044e\u0442 \u0433\u043b\u0443\u0431\u0438\u043d\u0443 \u0441\u0432\u043e\u0438\u0445 \u043e\u0448\u0438\u0431\u043e\u043a. \u041e\u0442\u0441\u044e\u0434\u0430 \u0440\u043e\u0436\u0434\u0430\u044e\u0442\u0441\u044f \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0435 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0438 \u0441\u0430\u043c\u043e\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u043e\u0431\u0451\u0440\u0442\u043a\u0438\/\u043a\u043e\u043c\u0431\u0430\u0439\u043d\u044b, \u0445\u043e\u0442\u044f \u0438\u043d\u0434\u0443\u0441\u0442\u0440\u0438\u044f \u0443\u0436\u0435 \u0434\u0430\u0432\u043d\u043e \u0432\u044b\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u0437\u0440\u0435\u043b\u044b\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b \u0438 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438: Playwright \u0434\u043b\u044f UI, <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a> \u0434\u043b\u044f HTTP, <a href=\"https:\/\/docs.pytest.org\/en\/stable\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a> \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432, <a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">locust<\/a> \u0434\u043b\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438. \u042d\u0442\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u0435 \u043d\u0443\u0436\u0434\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0431\u0451\u0440\u0442\u043a\u0430\u0445 \u043d\u0430 3500 \u0441\u0442\u0440\u043e\u043a \u0441 \u043a\u043e\u0441\u0442\u044b\u043b\u044f\u043c\u0438 \u0438 \u0445\u0430\u043a\u0430\u043c\u0438.<\/p>\n<h2>\u0427\u0435\u043c \u044d\u0442\u043e \u043f\u043b\u043e\u0445\u043e \u0434\u043b\u044f \u043d\u043e\u0432\u0438\u0447\u043a\u043e\u0432?<\/h2>\n<ul>\n<li>\n<p>\u0424\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043b\u043e\u0436\u043d\u043e\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435, \u0447\u0442\u043e \u00ab\u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u00bb \u2014 \u044d\u0442\u043e \u043d\u0430\u0431\u043e\u0440 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0445 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a>-\u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b (<code>sleep<\/code>, \u0441\u0442\u0440\u0435\u043b\u043a\u0438 \u0432\u043d\u0438\u0437, \u0434\u0443\u0431\u043b\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0439) \u0437\u0430\u043a\u0440\u0435\u043f\u043b\u044f\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u00ab\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0430\u00bb.<\/p>\n<\/li>\n<li>\n<p>SQL, API \u0438 UI \u0441\u043c\u0435\u0448\u0430\u043d\u044b \u0432 \u043e\u0434\u043d\u043e\u043c \u0444\u0430\u0439\u043b\u0435 \u2192 \u0441\u0442\u0438\u0440\u0430\u044e\u0442\u0441\u044f \u0433\u0440\u0430\u043d\u0438\u0446\u044b \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u0421\u0442\u0443\u0434\u0435\u043d\u0442 \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u043f\u043e\u043d\u0438\u043c\u0430\u0442\u044c, \u0433\u0434\u0435 UI, \u0433\u0434\u0435 \u0431\u0430\u0437\u0430, \u0430 \u0433\u0434\u0435 \u0441\u0435\u0440\u0432\u0438\u0441.<\/p>\n<\/li>\n<li>\n<p>\u0412 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0442\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043b\u043e\u043c\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0435\u0440\u0432\u043e\u043c \u0436\u0435 \u043a\u043e\u0434-\u0440\u0435\u0432\u044c\u044e \u0438\u043b\u0438 \u0441\u043e\u0431\u0435\u0441\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041d\u043e\u0432\u0438\u0447\u043a\u0443 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0449\u0435 \u00ab\u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u0438 \u0437\u0430\u0431\u044b\u0442\u044c\u00bb, \u043d\u043e \u044d\u0442\u043e \u043d\u0435 \u043e\u0431\u0443\u0447\u0435\u043d\u0438\u0435, \u0430 \u043f\u0440\u0438\u0432\u0438\u0432\u043a\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u0434\u0430. \u041f\u043e\u0442\u043e\u043c \u043f\u0440\u0438\u0434\u0451\u0442\u0441\u044f \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442\u044c \u00ab\u0434\u0435\u0442\u043e\u043a\u0441\u00bb: \u043f\u0435\u0440\u0435\u0443\u0447\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u0438 \u0437\u0430\u043d\u043e\u0432\u043e \u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043c\u044b\u0448\u043b\u0435\u043d\u0438\u0435.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0427\u0442\u043e \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442?<\/h2>\n<ul>\n<li>\n<p>\u042d\u0442\u043e \u043d\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430, \u0430 \u043d\u0435\u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u0443\u0442\u0438\u043b\u0438\u0442 \u0431\u0435\u0437 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b, \u0441 \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043a\u043e\u0434\u0430 \u0438 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c\u0438 \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043b\u043e\u0433\u0430\u043d \u00ab\u043f\u0438\u0448\u0438\u0442\u0435 \u043c\u0435\u043d\u044c\u0448\u0435 \u043a\u043e\u0434\u0430\u00bb \u0434\u043e\u0441\u0442\u0438\u0433\u0430\u0435\u0442\u0441\u044f \u043d\u0435 \u0433\u0440\u0430\u043c\u043e\u0442\u043d\u044b\u043c \u0434\u0438\u0437\u0430\u0439\u043d\u043e\u043c, \u0430 \u0442\u0435\u043c, \u0447\u0442\u043e \u0432\u0441\u0451 \u0437\u0430\u0432\u044f\u0437\u0430\u043d\u043e \u043d\u0430 \u043a\u043e\u0441\u0442\u044b\u043b\u0438 \u0438 \u0445\u0430\u043a\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0422\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0434\u043e\u043b\u0433 \u0437\u0430\u0448\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u044f\u043c\u043e \u0432 \u0433\u043e\u043b\u043e\u0432\u044b \u043d\u043e\u0432\u0438\u0447\u043a\u043e\u0432: \u043e\u043d\u0438 \u0438\u0441\u043a\u0440\u0435\u043d\u043d\u0435 \u0434\u0443\u043c\u0430\u044e\u0442, \u0447\u0442\u043e \u00ab\u0442\u0430\u043a \u0438 \u043d\u0430\u0434\u043e \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u043f\u044b\u0442\u043a\u0430 \u00ab\u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0451 \u0438 \u0441\u0440\u0430\u0437\u0443\u00bb \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0430\u0431\u0441\u0443\u0440\u0434\u0443: UI-\u043e\u0431\u0432\u044f\u0437\u043a\u0430 \u043d\u0430 <a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a>, SQL \u0434\u043b\u044f PostgreSQL\/MySQL\/SQLite, VPN, API \u043d\u0430 requests, \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0447\u0435\u0440\u0435\u0437 <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a> \u2014 \u0432\u0441\u0451 \u0432 \u043e\u0434\u043d\u043e\u043c \u0444\u0430\u0439\u043b\u0435.<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0430 \u0434\u0435\u043b\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043b\u0438 \u043b\u0438\u0448\u044c \u043c\u0430\u043b\u0443\u044e \u0447\u0430\u0441\u0442\u044c. \u0412\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u2014 \u044d\u0442\u043e 3500 \u0441\u0442\u0440\u043e\u043a \u043a\u043e\u0434\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0449\u0435 \u0432\u044b\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0441 \u043d\u0443\u043b\u044f, \u0447\u0435\u043c \u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c.<\/p>\n<h2>\u041a\u0430\u043a \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u044c\u0441\u044f?<\/h2>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u043a\u0438\u0435 \u0432\u0435\u0449\u0438 \u0432 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0435 \u0438\u043b\u0438 \u0434\u0430\u0436\u0435 \u043d\u0430 \u0443\u0447\u0435\u0431\u043d\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u2014 \u0440\u0438\u0441\u043a\u043e\u0432\u0430\u043d\u043d\u043e. \u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c \u2014 \u0440\u0430\u0441\u0441\u043c\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u00ab\u0441\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a\u00bb \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u0432\u043e\u043e\u0431\u0449\u0435 \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441 <a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a> \u0438\u043b\u0438 requests, \u0430 \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u0442\u044c \u043f\u043e\u0434 \u0437\u0430\u0434\u0430\u0447\u0443 \u0442\u043e\u0447\u0435\u0447\u043d\u043e.<\/p>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<blockquote>\n<p>\u041c\u044b \u0432\u0441\u0435 \u043a\u043e\u0433\u0434\u0430-\u0442\u043e \u043f\u0438\u0441\u0430\u043b\u0438 \u043a\u0440\u0438\u0432\u043e\u0439 \u043a\u043e\u0434. \u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u2014 \u0432\u043e\u0432\u0440\u0435\u043c\u044f \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u043e\u0442\u0432\u044b\u043a\u043d\u0443\u0442\u044c \u0438 \u043d\u0430\u0447\u0430\u0442\u044c \u043f\u0438\u0441\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u043f\u043e\u0445\u043e\u0436\u0438\u0445 \u0433\u0440\u0430\u0431\u043b\u0435\u0439 \u2014 \u043f\u043e\u0434\u0435\u043b\u0438\u0442\u0435\u0441\u044c \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u044f\u0445: \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043c \u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0432 \u0447\u0435\u043a\u043b\u0438\u0441\u0442<\/p>\n<\/blockquote>\n<p>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u043b\u0438, \u2014 \u044d\u0442\u043e \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u00ab\u0437\u0430\u0431\u0430\u0432\u043d\u044b\u0435 \u043a\u043e\u0441\u0442\u044b\u043b\u0438\u00bb. \u042d\u0442\u043e \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u0435\u0448\u0430\u044e\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0437\u0432\u0438\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442 \u0442\u0435\u0441\u0442\u044b \u0432 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0431\u043e\u043b\u0438 \u0438 \u043e\u0442\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u044e\u0442 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0434\u043e\u043b\u0433 \u043d\u0430 \u0433\u043e\u0434\u044b \u0432\u043f\u0435\u0440\u0451\u0434.<\/p>\n<p>\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u043c\u044b\u0441\u043b\u044c \u043f\u0440\u043e\u0441\u0442\u0430: \u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u044b \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043d\u0435 \u0441\u043b\u043e\u0436\u043d\u0435\u0435, \u0447\u0435\u043c \u043f\u0438\u0441\u0430\u0442\u044c \u0438\u0445 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043b\u0438\u0448\u044c \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u00ab\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0435\u00bb \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438 \u0434\u0430\u044e\u0442 \u043d\u0430\u0434\u0451\u0436\u043d\u044b\u0439, \u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0443\u0435\u043c\u044b\u0439 \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u0434, \u0430 \u00ab\u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0435\u00bb \u2014 flaky, \u0445\u0430\u043e\u0441 \u0438 \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0439 \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433.<\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0435 \u043f\u0443\u0442\u044c \u0432 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u2014 \u043e\u0440\u0438\u0435\u043d\u0442\u0438\u0440\u0443\u0439\u0442\u0435\u0441\u044c \u043d\u0430 \u0437\u0440\u0435\u043b\u044b\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b \u0438 \u0443\u0441\u0442\u043e\u044f\u0432\u0448\u0438\u0435\u0441\u044f \u043f\u043e\u0434\u0445\u043e\u0434\u044b:<\/p>\n<ul>\n<li>\n<p>Playwright \u0438\u043b\u0438 <a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a> \u0434\u043b\u044f UI,<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a> + <a href=\"https:\/\/docs.pytest.org\/en\/stable\/\" rel=\"noopener noreferrer nofollow\">pytest<\/a> \u0434\u043b\u044f API,<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/www.sqlalchemy.org\/\" rel=\"noopener noreferrer nofollow\">SQLAlchemy<\/a> \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0411\u0414,<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/locust.io\/\" rel=\"noopener noreferrer nofollow\">Locust<\/a> \u0438\u043b\u0438 <a href=\"https:\/\/k6.io\/\" rel=\"noopener noreferrer nofollow\">k6<\/a> \u0434\u043b\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u041e\u043d\u0438 \u0443\u0436\u0435 \u0440\u0435\u0448\u0430\u044e\u0442 90% \u0437\u0430\u0434\u0430\u0447 \u00ab\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438\u00bb \u0438 \u0438\u0437\u0431\u0430\u0432\u043b\u044f\u044e\u0442 \u0432\u0430\u0441 \u043e\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0442\u044c \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u00ab\u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434 \u043d\u0430 3500 \u0441\u0442\u0440\u043e\u043a\u00bb.<\/p>\n<p>\u0418 \u0441\u0430\u043c\u043e\u0435 \u0432\u0430\u0436\u043d\u043e\u0435: \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u044b \u2014 \u044d\u0442\u043e \u043a\u043e\u0434. \u041a \u043d\u0435\u043c\u0443 \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c\u044b \u0442\u0435 \u0436\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430, \u0447\u0442\u043e \u0438 \u043a \u0431\u043e\u0435\u0432\u043e\u043c\u0443 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0443: \u043c\u043e\u0434\u0443\u043b\u044c\u043d\u043e\u0441\u0442\u044c, \u0447\u0438\u0442\u0430\u0435\u043c\u043e\u0441\u0442\u044c, \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u043e\u0441\u0442\u044c, \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c. \u0427\u0435\u043c \u0440\u0430\u043d\u044c\u0448\u0435 \u0432\u044b \u044d\u0442\u043e \u0443\u0441\u0432\u043e\u0438\u0442\u0435, \u0442\u0435\u043c \u043c\u0435\u043d\u044c\u0448\u0435 \u0431\u0443\u0434\u0435\u0442 \u00ab\u0434\u0435\u0442\u043e\u043a\u0441\u0430\u00bb \u0432 \u0431\u0443\u0434\u0443\u0449\u0435\u043c.<\/p>\n<p>\u0422\u0430\u043a \u0447\u0442\u043e \u0435\u0441\u043b\u0438 \u0432\u0430\u043c \u043f\u043e\u043f\u0430\u0434\u0451\u0442\u0441\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430-\u00ab\u043a\u043e\u043c\u0431\u0430\u0439\u043d\u00bb \u0441 \u043c\u0430\u0433\u0438\u0435\u0439 \u0438 \u043a\u043e\u0441\u0442\u044b\u043b\u044f\u043c\u0438 \u2014 \u043d\u0435 \u0441\u043f\u0435\u0448\u0438\u0442\u0435 \u0440\u0430\u0434\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u00ab\u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b \u0441\u0442\u0430\u043b\u043e \u043f\u0440\u043e\u0449\u0435\u00bb. \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u044d\u0442\u043e \u043b\u043e\u0432\u0443\u0448\u043a\u0430. \u041b\u0443\u0447\u0448\u0435 \u043f\u043e\u0442\u0440\u0430\u0442\u0438\u0442\u044c \u0447\u0443\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043d\u0430 \u043e\u0441\u0432\u043e\u0435\u043d\u0438\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0430\u043a\u0442\u0438\u043a \u0438 \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b, \u0437\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0441\u0442\u044b\u0434\u043d\u043e \u043d\u0438 \u0432\u0430\u043c, \u043d\u0438 \u0432\u0430\u0448\u0435\u043c\u0443 \u043f\u0440\u043e\u0435\u043a\u0442\u0443.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/942532\/\"> https:\/\/habr.com\/ru\/articles\/942532\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435<\/h2>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u0440\u0430\u0437\u0431\u0435\u0440\u0443 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0442\u0438\u043f\u0438\u0447\u043d\u044b\u0445 \u043e\u0448\u0438\u0431\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u044e\u0442\u0441\u044f \u043f\u0440\u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432 \u043d\u0430 Python. \u0426\u0435\u043b\u044c \u043d\u0435 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u0441\u043c\u0435\u044f\u0442\u044c \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0445 \u043b\u044e\u0434\u0435\u0439 \u0438\u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u044b. \u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u2014 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0430\u0431\u0441\u0443\u0440\u0434\u043d\u043e\u0441\u0442\u044c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043f\u043e\u0434\u0445\u043e\u0434\u043e\u0432, \u043e\u0431\u044a\u044f\u0441\u043d\u0438\u0442\u044c, \u043a\u0430\u043a \u043d\u0435 \u0441\u0442\u043e\u0438\u0442 \u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u0443\u044e \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u0438 \u043f\u043e\u0447\u0435\u043c\u0443 \u044d\u0442\u043e \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c.<\/p>\n<p>\u0417\u0430\u0434\u0430\u0447\u0430 \u043f\u0440\u043e\u0441\u0442\u0430\u044f: \u0441\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u0442\u044c \u0432\u0430\u043c \u0432\u0440\u0435\u043c\u044f \u0438 \u0441\u0438\u043b\u044b. \u0427\u0442\u043e\u0431\u044b \u043d\u0435 \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u043f\u043e\u0442\u043e\u043c \u00ab\u043f\u0435\u0440\u0435\u0443\u0447\u0438\u0432\u0430\u0442\u044c\u0441\u044f\u00bb, \u0438\u0437\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043e\u0442 \u043a\u043e\u0441\u0442\u044b\u043b\u0435\u0439 \u0438 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442\u044c \u0431\u043e\u043b\u0435\u0437\u043d\u0435\u043d\u043d\u044b\u0439 \u0434\u0435\u0442\u043e\u043a\u0441 \u043e\u0442 \u0441\u0430\u043c\u043e\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u00ab\u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434\u043e\u0432\u00bb. \u0413\u043e\u0440\u0430\u0437\u0434\u043e \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u0435\u0435 \u0441 \u0441\u0430\u043c\u043e\u0433\u043e \u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0438\u0441\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b \u0442\u0430\u043a, \u0447\u0442\u043e\u0431\u044b \u043a\u043e\u0434 \u0431\u044b\u043b \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c, \u043f\u043e\u043d\u044f\u0442\u043d\u044b\u043c \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u043c.<\/p>\n<blockquote>\n<p><strong>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440.<\/strong> \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u043e\u0431\u043e\u0431\u0449\u0435\u043d\u044b \u0438 \u0441\u0438\u043d\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u044b; \u0446\u0435\u043b\u044c \u2014 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u0430 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u043e\u0432. \u041b\u044e\u0431\u044b\u0435 \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u044f \u0441 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u043c\u0438 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b. \u0412\u0441\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438 \u2014 \u043f\u0440\u043e \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0443 \u0438 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0438, \u0430 \u043d\u0435 \u043f\u0440\u043e \u043b\u044e\u0434\u0435\u0439.<\/p>\n<\/blockquote>\n<h2>\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u043a\u0438<\/h2>\n<p>\u042d\u0442\u0430 \u0441\u0442\u0430\u0442\u044c\u044f \u043f\u043e\u044f\u0432\u0438\u043b\u0430\u0441\u044c \u043d\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e. \u041d\u0435\u0434\u0430\u0432\u043d\u043e \u043a\u043e \u043c\u043d\u0435 \u043f\u0440\u0438\u0448\u0451\u043b \u0441\u0442\u0443\u0434\u0435\u043d\u0442 \u0441 \u043a\u0443\u0440\u0441\u0430 \u0438 \u0437\u0430\u0434\u0430\u043b \u0432\u043e\u043f\u0440\u043e\u0441: <em>\u00ab\u042f \u043d\u0430\u0448\u0451\u043b \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432. \u042d\u0442\u043e \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0430? \u0422\u0430\u043a \u0434\u0435\u043b\u0430\u044e\u0442?\u00bb<\/em><\/p>\n<p>\u041a\u043e\u0433\u0434\u0430 \u044f \u043e\u0442\u043a\u0440\u044b\u043b \u0441\u0441\u044b\u043b\u043a\u0443 \u0438 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u043b \u043a\u043e\u0434, \u0443\u0432\u0438\u0434\u0435\u043b \u043c\u043e\u043d\u043e\u043b\u0438\u0442\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441 \u043f\u0435\u0440\u0435\u043c\u0435\u0448\u0430\u043d\u043d\u044b\u043c\u0438 \u0437\u043e\u043d\u0430\u043c\u0438 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u041f\u0435\u0440\u0435\u0434\u043e \u043c\u043d\u043e\u0439 \u043e\u043a\u0430\u0437\u0430\u043b\u0430\u0441\u044c \u00ab\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u00bb, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u0437\u0438\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u0441\u0435\u0431\u044f \u043a\u0430\u043a \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u043e\u0432 \u00ab\u043d\u0430 \u0432\u0441\u0435 \u0441\u043b\u0443\u0447\u0430\u0438 \u0436\u0438\u0437\u043d\u0438\u00bb. \u0412\u043d\u0443\u0442\u0440\u0438 \u2014 \u043e\u0434\u0438\u043d-\u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0430 3500 \u0441\u0442\u0440\u043e\u043a, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u044b\u043b\u043e \u0437\u0430\u043f\u0438\u0445\u043d\u0443\u0442\u043e \u0432\u0441\u0451 \u043f\u043e\u0434\u0440\u044f\u0434: UI-\u0442\u0435\u0441\u0442\u044b, API-\u0442\u0435\u0441\u0442\u044b, \u043e\u0431\u0451\u0440\u0442\u043a\u0438, \u0442\u0443\u043b\u0437\u044b, \u0445\u0435\u043b\u043f\u0435\u0440\u044b, \u043d\u0430\u0433\u0440\u0443\u0437\u043e\u0447\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u0438 \u0434\u0430\u0436\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0435 \u0443\u0442\u0438\u043b\u0438\u0442\u044b. \u041f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f \u043d\u0435 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a, \u0430 \u043c\u043e\u043d\u043e\u043b\u0438\u0442 \u0431\u0435\u0437 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u044b.<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>\u0418 \u0441\u0430\u043c\u043e\u0435 \u0443\u0434\u0438\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435: \u0441\u043e \u0441\u043b\u043e\u0432 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430, \u044d\u0442\u043e\u0442 \u00ab\u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u00bb \u043f\u0440\u0435\u043f\u043e\u0434\u043d\u043e\u0441\u0438\u0442\u0441\u044f \u043a\u0430\u043a \u00ab\u043b\u0451\u0433\u043a\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u0438\u0441\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u0442\u0435\u0441\u0442\u044b\u00bb. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043c, \u043f\u043e\u0447\u0435\u043c\u0443 \u044d\u0442\u043e \u0441\u043e\u0432\u0441\u0435\u043c \u043d\u0435 \u043b\u0451\u0433\u043a\u0438\u0439 \u043f\u0443\u0442\u044c, \u0430 \u0441\u043a\u043e\u0440\u0435\u0435 \u0431\u044b\u0441\u0442\u0440\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c \u0442\u0435\u0441\u0442\u0430\u043c \u0438 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u043e\u043c\u0443 \u0434\u043e\u043b\u0433\u0443.<\/p>\n<blockquote>\n<p><strong>\u0421\u043a\u0430\u0436\u0443 \u0441\u0440\u0430\u0437\u0443:<\/strong> \u044f \u043d\u0435 \u0431\u0443\u0434\u0443 \u0434\u0430\u0432\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043e\u043a \u0438 \u043d\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u043e\u0432. \u0426\u0435\u043b\u044c \u0441\u0442\u0430\u0442\u044c\u0438 \u043d\u0435 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u043a\u043e\u0433\u043e-\u0442\u043e \u0432\u044b\u0441\u043c\u0435\u0438\u0432\u0430\u0442\u044c \u0438\u043b\u0438 \u0443\u043d\u0438\u0437\u0438\u0442\u044c. \u0426\u0435\u043b\u044c \u2014 \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u044b\u0435 \u043e\u0448\u0438\u0431\u043a\u0438, \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u0438\u0442\u044c \u043a\u043e\u0441\u0442\u044b\u043b\u0438, \u0432\u0435\u043b\u043e\u0441\u0438\u043f\u0435\u0434\u044b \u0438 \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b. \u041f\u043e\u0434\u043e\u0431\u043d\u044b\u0439 \u043a\u043e\u0434, \u0443\u0432\u044b, \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442\u0441\u044f \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0434\u0435\u0441\u044c: \u043e\u043d \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u0445, \u0434\u0430 \u0435\u0449\u0451 \u0438 \u043f\u043e\u0434\u0430\u0451\u0442\u0441\u044f \u043d\u043e\u0432\u0438\u0447\u043a\u0430\u043c \u043a\u0430\u043a \u00ab\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434\u00bb.<\/p>\n<\/blockquote>\n<p>\u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0432\u043c\u0435\u0441\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0434\u0451\u043c \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u00ab\u0434\u0435\u0442\u043e\u043a\u0441\u00bb \u043e\u0442 \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0445 \u0440\u0435\u0448\u0435\u043d\u0438\u0439.<\/p>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 1. \u00ab\u0422\u0430\u043d\u0446\u044b \u0441\u043e \u0441\u0442\u0440\u0435\u043b\u043e\u0447\u043a\u0430\u043c\u0438 \u0432\u043d\u0438\u0437\u00bb<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0412 \u043a\u043e\u0434\u0435 \u0434\u0435\u0441\u044f\u0442\u043a\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0432\u0438\u0434\u0430 \u00ab\u043d\u0430\u0436\u043c\u0438 \u0441\u0442\u0440\u0435\u043b\u043a\u0443 \u0432\u043d\u0438\u0437 N \u0440\u0430\u0437, \u0432\u0434\u0440\u0443\u0433 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u043e\u043a\u0430\u0436\u0435\u0442\u0441\u044f \u0432 \u0432\u0438\u0434\u0438\u043c\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438\u00bb. \u0427\u0430\u0441\u0442\u043e \u0435\u0449\u0451 \u0441 <code>time.sleep(0.1)<\/code> \u0432 \u0446\u0438\u043a\u043b\u0435 \u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u043e\u0439 \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c \u00ab\u043a\u043e\u0433\u0434\u0430 \u043f\u043e\u0432\u0435\u0437\u0451\u0442\u00bb.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">import time from selenium.webdriver import ActionChains from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC   def make_displayed_with_arrow_down_and_click(driver, xpath, waiting_time):     end = time.time() + waiting_time     while time.time() &lt; end:         try:             el = WebDriverWait(driver, 0.2).until(                 EC.visibility_of_element_located((By.XPATH, xpath))             )             if el.is_displayed():                 el.click()                 return True         except:             pass         ActionChains(driver).send_keys(Keys.ARROW_DOWN).perform()         time.sleep(0.1)     return False <\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>Flaky \u0438 \u0433\u043e\u043d\u043a\u0438. <code>time.sleep()<\/code> \u043c\u0430\u0441\u043a\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438, \u0430 \u043d\u0435 \u0440\u0435\u0448\u0430\u0435\u0442 \u0435\u0451. \u041d\u0430 CI \u0442\u0430\u043a\u0438\u0435 \u0442\u0435\u0441\u0442\u044b \u00ab\u043c\u0438\u0433\u0430\u044e\u0442\u00bb.<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u043e\u0442 \u0444\u043e\u043a\u0443\u0441\u0430. \u041a\u043b\u0430\u0432\u0438\u0448\u0438 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u0432 \u0444\u043e\u043a\u0443\u0441\u0435. \u041b\u044e\u0431\u043e\u0439 \u043f\u043e\u043f-\u0430\u043f\/\u043c\u043e\u0434\u0430\u043b \u2014 \u0438 \u0432\u0441\u0451 \u0441\u043b\u043e\u043c\u0430\u043b\u043e\u0441\u044c.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\/\u0440\u0430\u0437\u0434\u0443\u0432\u0430\u043d\u0438\u0435. \u0412\u0430\u0440\u0438\u0430\u0446\u0438\u0438 \u00ab\u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u0432\u043d\u0438\u0437\/\u0432\u0432\u0435\u0440\u0445\/ENTER\/SPACE\u00bb \u043f\u043b\u043e\u0434\u044f\u0442 \u0434\u0435\u0441\u044f\u0442\u043a\u0438 \u043e\u0434\u043d\u043e\u0442\u0438\u043f\u043d\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u0445\u043e\u0434 DOM-\u043c\u043e\u0434\u0435\u043b\u0438. \u0412\u043c\u0435\u0441\u0442\u043e \u044f\u0432\u043d\u043e\u0433\u043e \u0441\u043a\u0440\u043e\u043b\u043b\u0430 \u043a \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0443 \u2014 \u00ab\u043d\u0430\u0434\u0435\u0435\u043c\u0441\u044f\u00bb, \u0447\u0442\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0441\u0430\u043c\u0430 \u043f\u0440\u043e\u043c\u043e\u0442\u0430\u0435\u0442\u0441\u044f.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0435\u0448\u0435\u043d\u0438\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u0439. \u041f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u044f\u0432\u043d\u044b\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u2014 \u0438\u0442\u043e\u0433\u043e\u043c \u0441\u0442\u0430\u043d\u043e\u0432\u044f\u0442\u0441\u044f \u043d\u0435\u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0443\u0435\u043c\u044b\u0435 \u0442\u0430\u0439\u043c-\u0430\u0443\u0442\u044b.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e (\u043a\u043e\u0440\u043e\u0442\u043a\u043e \u0438 \u043d\u0430\u0434\u0451\u0436\u043d\u043e)<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u2014 Playwright<\/h4>\n<p>\u041f\u043e\u0447\u0435\u043c\u0443: \u0430\u0432\u0442\u043e\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u00ab\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438\u00bb, \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u044b, \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u043a\u0440\u043e\u043b\u043b, \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442 \u0441\u0435\u0442\u0438\/\u043a\u043e\u043d\u0441\u043e\u043b\u0438, \u043c\u0435\u043d\u044c\u0448\u0435 \u043a\u043e\u0434\u0430 \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 flaky.<\/p>\n<pre><code class=\"python\"># pip install playwright pytest-playwright # playwright install  from playwright.sync_api import Page  def click(page: Page, locator: str):     # Playwright \u0441\u0430\u043c \u0434\u043e\u0436\u0434\u0451\u0442\u0441\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438\/\u043a\u043b\u0438\u043a\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u0434\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442     page.locator(locator).click()  def type_text(page: Page, locator: str, text: str):     page.locator(locator).fill(text)  def get_text(page: Page, locator: str) -&gt; str:     return page.locator(locator).inner_text()  def click_in_scroll_container(page: Page, container: str):     container_locator = page.locator(container)     container_locator.scroll_into_view_if_needed()     container_locator.click() <\/code><\/pre>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u00ab\u0441\u0442\u0440\u0435\u043b\u043e\u043a \u0432\u043d\u0438\u0437\u00bb, <code>sleep(0.1)<\/code> \u0438 \u0448\u0430\u043c\u0430\u043d\u0441\u0442\u0432\u0430 \u0441 ActionChains.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u043a\u0430\u0442\u043e\u0440\u044b \u043b\u0443\u0447\u0448\u0435 \u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0435 XPath-\u00ab\u043f\u0440\u043e\u0441\u0442\u044b\u043d\u044f\u043c\u0438\u00bb, \u0430 \u0447\u0435\u0440\u0435\u0437 <code>data-test-id<\/code>:<br \/> <code>page.get_by_test_id(locator).click()<\/code>.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041a\u043e\u0433\u0434\u0430 \u0432\u0441\u0451-\u0442\u0430\u043a\u0438 Selenium?<\/h3>\n<p>\u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 \u0443\u0436\u0435 \u043d\u0430 <a href=\"https:\/\/www.selenium.dev\/\" rel=\"noopener noreferrer nofollow\">Selenium<\/a> \u0438 \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f, \u0441\u0432\u043e\u0434\u0438\u043c \u0443\u0442\u0438\u043b\u0438\u0442\u044b \u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c\u0443 \u0438 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u043a\u043b\u0430\u0432\u0438\u0448\u0438 \u043a\u0430\u043a \u043a\u043e\u0441\u0442\u044b\u043b\u0438:<\/p>\n<pre><code class=\"python\">from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import ElementClickInterceptedException  def click(driver, xpath: str, timeout: int = 10) -&gt; None:     locator = (By.XPATH, xpath)     el = WebDriverWait(driver, timeout).until(EC.element_to_be_clickable(locator))     driver.execute_script(\"arguments[0].scrollIntoView({block:'center', inline:'center'})\", el)     try:         el.click()     except ElementClickInterceptedException:         driver.execute_script(\"arguments[0].click()\", el)  # \u0440\u0435\u0434\u043a\u0438\u0439 \u0440\u0435\u0437\u0435\u0440\u0432  def type_text(driver, xpath: str, text: str, timeout: int = 10) -&gt; None:     el = WebDriverWait(driver, timeout).until(EC.visibility_of_element_located((By.XPATH, xpath)))     el.clear()     el.send_keys(text) <\/code><\/pre>\n<h3>\u041a\u043e\u0433\u0434\u0430 \u0443\u043c\u0435\u0441\u0442\u043d\u044b \u043a\u043b\u0430\u0432\u0438\u0448\u0438?<\/h3>\n<p>\u0422\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0432\u044b \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c\/\u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044e \u043a\u043b\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u043e\u0439 (Tab flow, \u043c\u0435\u043d\u044e-\u0441\u0442\u0440\u0435\u043b\u043a\u0438, \u0445\u043e\u0442\u043a\u0435\u0438). \u0414\u043b\u044f \u00ab\u043f\u0440\u043e\u0441\u043a\u0440\u043e\u043b\u043b\u0438\u0442\u044c \u0438 \u043a\u043b\u0438\u043a\u043d\u0443\u0442\u044c\u00bb \u2014 \u044d\u0442\u043e \u0430\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d.<\/p>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442 \u0432\u043c\u0435\u0441\u0442\u043e \u00ab\u0442\u0430\u043d\u0446\u0435\u0432\u00bb<\/h3>\n<ul>\n<li>\n<p>Playwright \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0430\u0432\u0442\u043e\u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f, \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0435 \u043b\u043e\u043a\u0430\u0442\u043e\u0440\u044b).<\/p>\n<\/li>\n<li>\n<p>\u0415\u0441\u043b\u0438 Selenium \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u044f\u0432\u043d\u044b\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f + <code>scrollIntoView<\/code>, \u0431\u0435\u0437 <code>sleep<\/code>.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0434\u0438\u043d-\u0434\u0432\u0430 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0445 \u0445\u0435\u043b\u043f\u0435\u0440\u0430 \u0432\u043c\u0435\u0441\u0442\u043e \u0434\u0435\u0441\u044f\u0442\u043a\u043e\u0432 \u00ab\u0441\u0442\u0440\u0435\u043b\u043a\u0430 \u0432\u043d\u0438\u0437 N \u0440\u0430\u0437\u00bb.<\/p>\n<\/li>\n<li>\n<p>JS-\u043a\u043b\u0438\u043a \u2014 \u043a\u0430\u043a \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, \u0430 \u043d\u0435 \u043a\u0430\u043a \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044f.<\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 2. \u00abexec \u0432 API\u00bb \u0438 \u043f\u0440\u043e\u0447\u0430\u044f \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043c\u0430\u0433\u0438\u044f<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u0424\u0443\u043d\u043a\u0446\u0438\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 HTTP-\u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c, \u0441\u043c\u0435\u0448\u0438\u0432\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u0438 \u043d\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0438.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\">def post_request(requests_url: str, requests_body: dict, requests_headers: dict,                  pre_script: str = None, auth: list = None):     # \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 \u00ab\u0434\u043b\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438\u00bb \ud83e\udd26     if pre_script is not None:         exec(pre_script)      body = json.dumps(requests_body)     response = requests.post(requests_url, auth=auth, data=body, headers=requests_headers)      if response.status_code in (200, 201):         print('POST request successful')         return response.json()     else:         print('POST request failed')         return response.json()<\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p><code>exec(pre_script)<\/code> \u2014 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0438\u0437 \u0441\u0442\u0440\u043e\u043a\u0438. \u042d\u0442\u043e \u0443\u044f\u0437\u0432\u0438\u043c\u043e\u0441\u0442\u044c \u043a\u043b\u0430\u0441\u0441\u0430 RCE. \u0414\u043e\u0432\u0435\u0440\u0438\u0435 \u043a \u0434\u0430\u043d\u043d\u044b\u043c \u2260 \u043f\u043e\u0432\u043e\u0434 \u0438\u0445 \u0438\u0441\u043f\u043e\u043b\u043d\u044f\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0435\u0448\u0435\u043d\u0438\u0435 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u0412 \u043e\u0434\u043d\u043e\u043c \u043c\u0435\u0442\u043e\u0434\u0435 \u00ab\u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0430 \u043f\u0440\u0435\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0438\u043d\u0433\u0430\u00bb, \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f, \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0432\u044b\u0437\u043e\u0432 \u0438 \u00ab\u043c\u043e\u043b\u0447\u0430\u043b\u0438\u0432\u043e\u0435\u00bb \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.<\/p>\n<\/li>\n<li>\n<p><code>data=body<\/code> \u0432\u043c\u0435\u0441\u0442\u043e <code>json=...<\/code> \u2014 \u0440\u0438\u0441\u043a\u0443\u0435\u0442\u0435 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u043c <code>Content-Type<\/code> \u0438 \u043a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u043e\u0439 (\u0438 \u0440\u0443\u0447\u043d\u043e\u0439 \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0442\u0430\u043c, \u0433\u0434\u0435 \u043e\u043d\u0430 \u043d\u0435 \u043d\u0443\u0436\u043d\u0430).<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u043e\u0432\/\u0440\u0435\u0442\u0440\u0430\u0435\u0432 \u2014 \u043f\u043e\u0434\u0432\u0438\u0441\u0430\u043d\u0438\u044f \u0438 flaky \u043d\u0430 CI.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u0442 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430. \u041d\u0435\u044f\u0441\u043d\u043e, \u0447\u0442\u043e \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043c\u0435\u0442\u043e\u0434, \u043a\u0430\u043a \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c 4xx\/5xx.<\/p>\n<\/li>\n<\/ul>\n<figure class=\"full-width\"><\/figure>\n<h3>\u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e?<\/h3>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 1. \u041d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u00ab\u0447\u0438\u0441\u0442\u044b\u0439\u00bb \u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0430 httpx<\/h4>\n<pre><code class=\"python\">import httpx   class HTTPClient:     def __init__(self, client: httpx.Client):         self.client = client      def post(self, url: str, json: dict, headers: dict | None = None) -&gt; httpx.Response:         return self.client.post(url, json=json, headers=headers)      def close(self):         self.client.close()  # \u043f\u0440\u0438\u043c\u0435\u0440 client = HTTPClient(httpx.Client(timeout=5)) resp = client.post(\"https:\/\/api.example.com\/login\", json={\"user\": \"foo\"}) print(resp.status_code, resp.json())<\/code><\/pre>\n<h4>\u0412\u0430\u0440\u0438\u0430\u043d\u0442 2. \u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 + \u0440\u0435\u0442\u0440\u0430\u0438 (\u043a\u043e\u0440\u043e\u0442\u043a\u043e)<\/h4>\n<pre><code class=\"python\">import httpx from backoff import on_exception, expo  # pip install backoff   class HTTPClient:     def __init__(self, client: httpx.AsyncClient):         self.client = client      @on_exception(expo, (httpx.TimeoutException, httpx.ConnectError), max_tries=3)     async def post(self, url: str, json: dict, headers: dict | None = None) -&gt; httpx.Response:         return await self.client.post(url, json=json, headers=headers)      async def aclose(self):         await self.client.aclose()<\/code><\/pre>\n<h4>(\u041e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e) \u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0447\u0435\u0440\u0435\u0437 Pydantic<\/h4>\n<pre><code class=\"python\">from pydantic import BaseModel  class LoginRequest(BaseModel):     username: str     password: str  class LoginResponse(BaseModel):     access_token: str     token_type: str = \"Bearer\"  # \u043f\u0440\u0438\u043c\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f payload = LoginRequest(username=\"foo\", password=\"bar\").model_dump() resp = client.post(\"https:\/\/api.example.com\/login\", json=payload) parsed = LoginResponse.model_validate_json(resp.text)<\/code><\/pre>\n<h3>\u041c\u0438\u043d\u0438-\u0447\u0435\u043a\u043b\u0438\u0441\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0438 \u0437\u0434\u0440\u0430\u0432\u043e\u0433\u043e \u0441\u043c\u044b\u0441\u043b\u0430<\/h3>\n<ul>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 <a href=\"https:\/\/docs.python.org\/3\/library\/functions.html#exec\" rel=\"noopener noreferrer nofollow\">exec<\/a>, <a href=\"https:\/\/docs.python.org\/3\/library\/functions.html#eval\" rel=\"noopener noreferrer nofollow\">eval<\/a>, \u00ab\u043f\u0440\u0435\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432\u00bb \u0441\u0442\u0440\u043e\u043a\u043e\u0439.<\/p>\n<\/li>\n<li>\n<p>\u0421\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u2014 \u0447\u0435\u0440\u0435\u0437 <code>json=<\/code>; \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0437\u0430\u0434\u0430\u0451\u043c \u044f\u0432\u043d\u043e, \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p>\u0412\u0441\u0435\u0433\u0434\u0430 \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u044b; \u0434\u043b\u044f \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u0442\u0435\u0439 \u2014 \u0440\u0435\u0442\u0440\u0430\u0438 \u0441 \u044d\u043a\u0441\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0439.<\/p>\n<\/li>\n<li>\n<p>\u0415\u0434\u0438\u043d\u044b\u0439 \u0438 \u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0443\u0435\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 (\u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f).<\/p>\n<\/li>\n<li>\n<p>\u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0432\u0445\u043e\u0434\u0430\/\u0432\u044b\u0445\u043e\u0434\u0430 (<a href=\"https:\/\/docs.pydantic.dev\/latest\/\" rel=\"noopener noreferrer nofollow\">Pydantic<\/a>) \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 \u0441\u044e\u0440\u043f\u0440\u0438\u0437\u043e\u0432 \u0432 \u0442\u0435\u0441\u0442\u0430\u0445.<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0433\u0438: \u043c\u0435\u0442\u043e\u0434, URL, \u0441\u0442\u0430\u0442\u0443\u0441, latency (\u0431\u0435\u0437 \u0443\u0442\u0435\u0447\u0435\u043a \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445).<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c <a href=\"https:\/\/peps.python.org\/pep-0760\/\" rel=\"noopener noreferrer nofollow\">bare except<\/a>: \u043b\u043e\u0432\u0438\u0442\u0435 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f <a href=\"https:\/\/www.python-httpx.org\/\" rel=\"noopener noreferrer nofollow\">httpx<\/a>\/<a href=\"https:\/\/requests.readthedocs.io\/en\/latest\/\" rel=\"noopener noreferrer nofollow\">requests<\/a><\/p>\n<\/li>\n<\/ul>\n<h2>\u0410\u043d\u0442\u0438\u043f\u0430\u0442\u0442\u0435\u0440\u043d 3. \u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 connection\/cursor<\/h2>\n<p><strong>\u0421\u0438\u043c\u043f\u0442\u043e\u043c.<\/strong> \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0411\u0414 \u0438 \u043a\u0443\u0440\u0441\u043e\u0440 \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u00ab\u0433\u0434\u0435-\u0442\u043e \u0441\u0432\u0435\u0440\u0445\u0443\u00bb, \u043a\u043b\u0430\u0434\u0443\u0442\u0441\u044f \u0432 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0438 \u0434\u0430\u043b\u044c\u0448\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0438\u0437 \u043b\u044e\u0431\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438.<\/p>\n<h3>\u041f\u043b\u043e\u0445\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (\u0441\u043e\u043a\u0440\u0430\u0449\u0451\u043d\u043d\u043e)<\/h3>\n<pre><code class=\"python\"># \u0433\u0434\u0435-\u0442\u043e \u0432 \u043c\u043e\u0434\u0443\u043b\u0435 global connection, cursor connection = psycopg2.connect(host=..., user=..., password=..., dbname=...) cursor = connection.cursor()  def export_table(...):     cursor.execute(sql)          # \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u0443\u0440\u0441\u043e\u0440     rows = cursor.fetchall()     ... # \u0432 finally \u0433\u0434\u0435-\u043d\u0438\u0431\u0443\u0434\u044c \u043d\u0438\u0436\u0435: cursor.close(); connection.close()<\/code><\/pre>\n<h4>\u0427\u0442\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0442\u0430\u043a?<\/h4>\n<ul>\n<li>\n<p>\u0423\u0442\u0435\u0447\u043a\u0438 \u0438<\/p>\n<\/li>\n<\/ul>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-473050","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/473050","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=473050"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/473050\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=473050"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=473050"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=473050"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}