{"id":335864,"date":"2022-07-18T21:00:08","date_gmt":"2022-07-18T21:00:08","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=335864"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=335864","title":{"rendered":"<span>\u0422\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c \u0441\u043b\u043e\u0439 \u0411\u0414 \u0432 Python \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c pytest \u0438 testcontainers<\/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<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/ge\/js\/_l\/gejs_lh3eulyddiuwlqo-ozutvo.png\" width=\"auto\" height=\"auto\" data-src=\"https:\/\/habrastorage.org\/webt\/ge\/js\/_l\/gejs_lh3eulyddiuwlqo-ozutvo.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u0431\u043e\u043b\u044c\u0448\u0443\u044e \u043f\u043e\u043f\u0443\u043b\u044f\u0440\u043d\u043e\u0441\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 <code>testcontainers<\/code> \u0432 \u043c\u0438\u0440\u0435 java, \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432 \u0441\u0435\u0442\u0438 \u043f\u043e  \u0435\u0451 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044e \u0432 python \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0435\u0442. \u0414\u0430\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f &#8212; \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043b\u0438\u043a\u0432\u0438\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u043f\u0440\u043e\u0431\u0435\u043b. \u042f \u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e pytest \u0438 testcontainers, \u0447\u0442\u043e \u044d\u0442\u043e \u0442\u0430\u043a\u043e\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0435, \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u043a\u0430\u0436\u0443 \u043f\u0440\u0438\u043c\u0435\u0440 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u044d\u0442\u043e \u0432\u043e\u0435\u0434\u0438\u043d\u043e. <\/p>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0411\u0414 \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PostgreSQL. \u0412 \u0441\u0435\u0442\u0438 \u0435\u0441\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 <a href=\"https:\/\/readthedocs.org\/projects\/testcontainers-python\/downloads\/pdf\/stable\/\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440<\/a> \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f testcontainers \u0441 PostgreSQL:<\/p>\n<pre><code class=\"python\">with PostgresContainer(\"postgres:9.5\") as postgres:   e = sqlalchemy.create_engine(postgres.get_connection_url())   result = e.execute(\"select version()\")<\/code><\/pre>\n<p>\u0414\u0430, \u043d\u0435 \u043c\u043d\u043e\u0433\u043e. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0437\u043e\u0432\u044c\u0451\u043c \u044d\u0442\u043e\u0442 \u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438. <\/p>\n<h3>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h3>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u0430, \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0447\u0430\u0441\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0432 \u0434\u0432\u0443\u0445 \u0411\u0414 \u0438 \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c \u0435\u0433\u043e. \u0412 \u043d\u0430\u0448\u0435\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u043c\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0442\u0440\u0438 \u043c\u0435\u0442\u043e\u0434\u0430 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u0411\u0414, \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u0438 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0411\u0414, \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0441\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u044b \u0438 \u0432\u0441\u0451 \u0442\u043e, \u0447\u0442\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b. \u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/yb\/o4\/nc\/ybo4nca1425utjqy6h1d6-tiejg.png\" width=\"auto\" height=\"auto\" data-src=\"https:\/\/habrastorage.org\/webt\/yb\/o4\/nc\/ybo4nca1425utjqy6h1d6-tiejg.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u0435 \u043f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u044f \u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435:<\/p>\n<ul>\n<li>\n<p><strong>db_services<\/strong> &#8212; \u043f\u0430\u043a\u0435\u0442, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0440\u0430\u0441\u043f\u043e\u043b\u0430\u0433\u0430\u044e\u0442\u0441\u044f \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0411\u0414<\/p>\n<\/li>\n<li>\n<p><strong>processing<\/strong> &#8212; \u043f\u0430\u043a\u0435\u0442, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/p>\n<\/li>\n<li>\n<p><strong>tests<\/strong> &#8212; \u043f\u0430\u043a\u0435\u0442, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u0432\u0441\u0451 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e SQL &#8212; \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438 \u043d\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u044b.<\/p>\n<p>\u0417\u0430 \u043e\u0441\u043d\u043e\u0432\u0443 \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u043e\u0437\u044c\u043c\u0451\u043c \u043a\u0443\u0441\u043e\u0447\u0435\u043a \u043e\u0442 <a href=\"https:\/\/postgrespro.ru\/education\/demodb\" rel=\"noopener noreferrer nofollow\">\u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445<\/a> \u043e\u0442 PostgresPro \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0431\u0430\u0437\u044b:<\/p>\n<\/li>\n<\/ul>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/06\/7g\/wn\/067gwn76fcwowyp7-96ljd_tx_c.png\" alt=\"\u0411\u0414 Airline\" title=\"\u0411\u0414 Airline\" width=\"auto\" height=\"auto\" data-src=\"https:\/\/habrastorage.org\/webt\/06\/7g\/wn\/067gwn76fcwowyp7-96ljd_tx_c.png\"\/><figcaption>\u0411\u0414 Airline<\/figcaption><\/figure>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/y0\/t_\/ox\/y0t_oxw1n8rbm2cz84qreubejjo.png\" alt=\"\u0411\u0414 Bookings\" title=\"\u0411\u0414 Bookings\" width=\"auto\" height=\"auto\" data-src=\"https:\/\/habrastorage.org\/webt\/y0\/t_\/ox\/y0t_oxw1n8rbm2cz84qreubejjo.png\"\/><figcaption>\u0411\u0414 Bookings<\/figcaption><\/figure>\n<h3>\u041c\u043e\u0434\u0443\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p>\u0414\u0430\u0431\u044b \u043d\u0435 \u043f\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0430\u0442\u044c \u043f\u043e\u0441\u0442 \u043a\u043e\u0434\u043e\u043c, \u0443\u0431\u0435\u0440\u0443 \u043c\u043e\u0434\u0443\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u0434 \u0441\u043f\u043e\u0439\u043b\u0435\u0440.<\/p>\n<details class=\"spoiler\">\n<summary>\u041c\u043e\u0434\u0443\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/summary>\n<div class=\"spoiler__content\">\n<p><strong>engine_factory.py<br \/><\/strong>\u0412 \u043c\u043e\u0434\u0443\u043b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0444\u0430\u0431\u0440\u0438\u043a\u0430 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 \u043a\u043e \u0432\u0441\u0435\u043c \u0411\u0414 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043f\u043e \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0443 <em>singleton. <\/em>\u0422\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0434\u043d\u0438 \u0438 \u0442\u0435 \u0436\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043a \u0411\u0414 \u0438\u0437 \u043b\u044e\u0431\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442\u043d\u044b\u0445 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439 \u0432 \u0432\u0438\u0434\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439.<\/p>\n<pre><code class=\"python\">import sqlalchemy.engine from sqlalchemy import create_engine   class MetaSingleton(type):     _instances = {}      def __call__(cls, *args, **kwargs):         if cls not in cls._instances:             cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)         return cls._instances[cls]   class EngineFactory(metaclass=MetaSingleton):     connections, db_urls = ({},) * 2     user, passw, stand, db_name = (None,) * 4      def get_engine(self, db_name, schema_name=None) -> sqlalchemy.engine.Engine:         self.db_name = db_name          if None in (self.user, self.stand, self.db_name):             raise ValueError('\u041d\u0435 \u0437\u0430\u0434\u0430\u043d\u044b \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b: stand, user, db_name')          if self.connections.get((db_name, schema_name)):             return self.connections.get((db_name, schema_name))         else:             url = self.get_postgres_url(db_name)             if schema_name:                 self.connections[(db_name, schema_name)] = create_engine(url, echo=False, echo_pool=False,                                                                          connect_args={                                                                              'options': f'-csearch_path={schema_name}'})             else:                 self.connections[(db_name, schema_name)] = create_engine(url, echo=False, echo_pool=False)         return self.connections[(db_name, schema_name)]      def dispose_engines(self) -> None:         for engine in self.connections.values():             engine.dispose()         self.connections = {}      def add_db(self, base_name, url):         self.db_urls[base_name] = url      def get_postgres_url(self, base_name) -> str:         stand = self.stand.lower()          if stand == 'localhost':             return self.db_urls.get(base_name) if self.db_urls.get(base_name) else ValueError(                 f'''URL \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 stand={stand}, db_name='{base_name}' \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d ''') <\/code><\/pre>\n<p><strong>db_service.py<\/strong><br \/>\u0417\u0434\u0435\u0441\u044c \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0434\u0432\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0411\u0414 (GET \u0438 POST), \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0443\u043f\u0440\u043e\u0441\u0442\u044f\u0442 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a \u0411\u0414 \u0447\u0435\u0440\u0435\u0437 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439.<\/p>\n<pre><code class=\"python\">from .engine_factory import EngineFactory engine = EngineFactory()   def get_from_postgres(sql, db_name, schema_name=None) -> list:     result = []     pg_engine = engine.get_engine(db_name=db_name, schema_name=schema_name)     try:         with pg_engine.connect() as connection:             cursor = connection.execution_options(stream_result=True).execute(sql)             for row in cursor:                 result.append(list(row))     except Exception as e:         raise RuntimeError(f'\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438 \u043a \u0411\u0414: {e}')     return result   def post_to_postgres(sql, db_name, schema_name=None) -> int:     pg_engine = engine.get_engine(db_name=db_name, schema_name=schema_name)     rows_processed = 0     try:         with pg_engine.connect() as connection:             cursor = connection.execution_options(stream_result=True, isolation_level='AUTOCOMMIT').execute(sql)             rows_processed = cursor.rowcount             cursor.close()     except Exception as e:         raise RuntimeError(f'\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 {sql} \u0432 \u0411\u0414: {e}')     return rows_processed<\/code><\/pre>\n<p><strong>airline.py<\/strong><br \/>\u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u043c\u043e\u0434\u0443\u043b\u0435 \u043e\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434\u044b, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0432 \u0411\u0414 <em>Airline<\/em><\/p>\n<pre><code class=\"python\">from db_services.db_service import get_from_postgres from db_services.db_service import post_to_postgres  DB_NAME = 'airline'  # \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u0440\u0435\u0439\u0441\u0430 def set_flight_status(flight_id, status) -> int:     sql = '''     update airline.flights         set status = '%s'         where flight_id = %d     ''' % (status, flight_id)      try:         return post_to_postgres(sql=sql, db_name=DB_NAME)     except Exception as e:         raise RuntimeError(e)  # \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0440\u0435\u0439\u0441\u0430 def get_flight_status(flight_id) -> list:     sql = '''     select status         from airline.flights         where flight_id = %d     ''' % flight_id      try:         return get_from_postgres(sql=sql, db_name=DB_NAME)     except Exception as e:         raise RuntimeError(e)<\/code><\/pre>\n<p><strong>bookings.py<\/strong><br \/>\u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u043c\u043e\u0434\u0443\u043b\u0435 \u043e\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434\u044b, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0432 \u0411\u0414 <em>Bookings<\/em><\/p>\n<pre><code class=\"python\">from db_services.db_service import get_from_postgres  DB_NAME = 'bookings'  # \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0430\u0441\u0441\u0430\u0436\u0438\u0440\u043e\u0432, \u0442\u0440\u0430\u0442\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u043e\u043b\u0435\u0435 limit def get_premium_psg_list(limit) -> list:     sql = '''             select passenger_name, sum(amount)                     from bookings.tickets                              join bookings.ticket_flights using (ticket_id)                     group by 1                     having sum(amount) > %d ''' % limit      try:         return get_from_postgres(sql=sql, db_name=DB_NAME)     except Exception as e:         raise RuntimeError(e) <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h3>\u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0411\u0414<\/h3>\n<p>\u041e\u0447\u0435\u0432\u0438\u0434\u043d\u043e, \u0447\u0442\u043e \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0435\u043c\u0430\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0411\u0414 \u0434\u043e\u043b\u0436\u043d\u0430 \u043e\u0442\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432\u043e\u0439 \u0411\u0414, \u043f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u043e\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0414\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0411\u0414, \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c ORM \u0441 \u0434\u0435\u043a\u043b\u0430\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u043c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043a\u043b\u0430\u0441\u0441\u043e\u0432.<\/p>\n<h4>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0438 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0411\u0414 Airline<\/h4>\n<details class=\"spoiler\">\n<summary>airline_db.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">from sqlalchemy import Column, String, INTEGER, TEXT, ForeignKey from sqlalchemy.ext.declarative import declarative_base  Base = declarative_base()   class Aircrafts(Base):     __tablename__ = 'aircrafts'     __table_args__ = {'schema': 'airline'}      aircraft_code = Column(String(3), nullable=False, primary_key=True, comment='\u041a\u043e\u0434 \u0441\u0430\u043c\u043e\u043b\u0435\u0442\u0430, IATA')     model = Column(TEXT, nullable=False, comment='\u041c\u043e\u0434\u0435\u043b\u044c \u0441\u0430\u043c\u043e\u043b\u0435\u0442\u0430')     range = Column(INTEGER, nullable=False, comment='\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0434\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043f\u043e\u043b\u0435\u0442\u0430, \u043a\u043c')   class Flights(Base):     __tablename__ = 'flights'     __table_args__ = {'schema': 'airline'}      flight_id = Column(INTEGER, nullable=False, primary_key=True, comment='\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0440\u0435\u0439\u0441\u0430')     flight_no = Column(String(10), nullable=False, comment='\u041d\u043e\u043c\u0435\u0440 \u0440\u0435\u0439\u0441\u0430')     aircraft_code = Column(String(3), ForeignKey('airline.aircrafts.aircraft_code'), nullable=False,                            comment='\u041a\u043e\u0434 \u0441\u0430\u043c\u043e\u043b\u0435\u0442\u0430, IATA')     status = Column(String(20), nullable=False, comment='\u0421\u0442\u0430\u0442\u0443\u0441 \u0440\u0435\u0439\u0441\u0430')   AIRCRAFTS_ROWS = [     {         \"aircraft_code\": \"773\",         \"model\": \"Boeing 777-300\",         \"range\": 11100     },     {         \"aircraft_code\": \"763\",         \"model\": \"Boeing 767-300\",         \"range\": 7900     },     {         \"aircraft_code\": \"SU9\",         \"model\": \"Sukhoi Superjet-100\",         \"range\": 3000     },     {         \"aircraft_code\": \"320\",         \"model\": \"Airbus A320-200\",         \"range\": 5700     },     {         \"aircraft_code\": \"321\",         \"model\": \"Airbus A321-200\",         \"range\": 5600     },     {         \"aircraft_code\": \"319\",         \"model\": \"Airbus A319-100\",         \"range\": 6700     },     {         \"aircraft_code\": \"733\",         \"model\": \"Boeing 737-300\",         \"range\": 4200     },     {         \"aircraft_code\": \"CN1\",         \"model\": \"Cessna 208 Caravan\",         \"range\": 1200     },     {         \"aircraft_code\": \"CR2\",         \"model\": \"Bombardier CRJ-200\",         \"range\": 2700     } ]  FLIGHTS_ROWS = [     {         \"flight_id\": 32959,         \"flight_no\": \"PG0550\",         \"aircraft_code\": \"CR2\",         \"status\": \"On Time\"     },     {         \"flight_id\": 28948,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33116,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"On Time\"     },     {         \"flight_id\": 33117,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33111,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Scheduled\"     },     {         \"flight_id\": 28929,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33052,         \"flight_no\": \"PG0359\",         \"aircraft_code\": \"CR2\",         \"status\": \"Cancelled\"     },     {         \"flight_id\": 33043,         \"flight_no\": \"PG0359\",         \"aircraft_code\": \"CR2\",         \"status\": \"On Time\"     },     {         \"flight_id\": 33118,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 30007,         \"flight_no\": \"PG0386\",         \"aircraft_code\": \"SU9\",         \"status\": \"Delayed\"     },     {         \"flight_id\": 28913,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33099,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Cancelled\"     },     {         \"flight_id\": 32207,         \"flight_no\": \"PG0425\",         \"aircraft_code\": \"CN1\",         \"status\": \"Departed\"     },     {         \"flight_id\": 33115,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33107,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Scheduled\"     },     {         \"flight_id\": 32806,         \"flight_no\": \"PG0080\",         \"aircraft_code\": \"CN1\",         \"status\": \"Cancelled\"     },     {         \"flight_id\": 32961,         \"flight_no\": \"PG0550\",         \"aircraft_code\": \"CR2\",         \"status\": \"Cancelled\"     },     {         \"flight_id\": 31611,         \"flight_no\": \"PG0494\",         \"aircraft_code\": \"CN1\",         \"status\": \"Delayed\"     },     {         \"flight_id\": 28895,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 30961,         \"flight_no\": \"PG0004\",         \"aircraft_code\": \"CR2\",         \"status\": \"Delayed\"     },     {         \"flight_id\": 31946,         \"flight_no\": \"PG0193\",         \"aircraft_code\": \"CN1\",         \"status\": \"Departed\"     },     {         \"flight_id\": 28904,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 28915,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33114,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33119,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Scheduled\"     },     {         \"flight_id\": 32863,         \"flight_no\": \"PG0080\",         \"aircraft_code\": \"CN1\",         \"status\": \"On Time\"     },     {         \"flight_id\": 33112,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Scheduled\"     },     {         \"flight_id\": 32898,         \"flight_no\": \"PG0147\",         \"aircraft_code\": \"SU9\",         \"status\": \"On Time\"     },     {         \"flight_id\": 28939,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33121,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Scheduled\"     },     {         \"flight_id\": 31363,         \"flight_no\": \"PG0619\",         \"aircraft_code\": \"CN1\",         \"status\": \"Delayed\"     },     {         \"flight_id\": 32083,         \"flight_no\": \"PG0708\",         \"aircraft_code\": \"733\",         \"status\": \"Departed\"     },     {         \"flight_id\": 28935,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 28942,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 31867,         \"flight_no\": \"PG0304\",         \"aircraft_code\": \"SU9\",         \"status\": \"Departed\"     },     {         \"flight_id\": 28912,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 32871,         \"flight_no\": \"PG0616\",         \"aircraft_code\": \"SU9\",         \"status\": \"Cancelled\"     },     {         \"flight_id\": 32937,         \"flight_no\": \"PG0147\",         \"aircraft_code\": \"SU9\",         \"status\": \"Departed\"     },     {         \"flight_id\": 33120,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 32247,         \"flight_no\": \"PG0604\",         \"aircraft_code\": \"CR2\",         \"status\": \"Delayed\"     } ]  AIRLINE_ROWS = {     Aircrafts: AIRCRAFTS_ROWS,     Flights: FLIGHTS_ROWS } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h4>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0438 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0411\u0414 Bookings<\/h4>\n<details class=\"spoiler\">\n<summary>bookings_db.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">from sqlalchemy import Column, String, INTEGER, BIGINT, TEXT, ForeignKey, PrimaryKeyConstraint, NUMERIC from sqlalchemy.ext.declarative import declarative_base  Base = declarative_base()   class Ticket(Base):     __tablename__ = 'tickets'     __table_args__ = {'schema': 'bookings'}      ticket_id = Column(BIGINT, nullable=False, unique=True, autoincrement=True, primary_key=True,                        comment='\u041d\u043e\u043c\u0435\u0440 \u0431\u0438\u043b\u0435\u0442\u0430')     passenger_id = Column(String(20), nullable=False, comment='\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0430\u0441\u0441\u0430\u0436\u0438\u0440\u0430')     passenger_name = Column(TEXT, nullable=False, comment='\u0418\u043c\u044f \u043f\u0430\u0441\u0441\u0430\u0436\u0438\u0440\u0430')   class Ticket_Flights(Base):     __tablename__ = 'ticket_flights'     __table_args__ = {'schema': 'bookings'}      ticket_id = Column(BIGINT, ForeignKey('bookings.tickets.ticket_id'), nullable=False, unique=True,                        comment='\u041d\u043e\u043c\u0435\u0440 \u0431\u0438\u043b\u0435\u0442\u0430')     flight_id = Column(INTEGER, nullable=False, comment='\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0440\u0435\u0439\u0441\u0430')     amount = Column(NUMERIC(10, 2), nullable=False, comment='\u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u043b\u0435\u0442\u0430')     PrimaryKeyConstraint(ticket_id, flight_id)   class Boarding_Passes(Base):     __tablename__ = 'boarding_passes'     __table_args__ = {'schema': 'bookings'}      boarding_no = Column(BIGINT, nullable=False, unique=True, autoincrement=True, primary_key=True,                          comment='\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0441\u0430\u0434\u043e\u0447\u043d\u043e\u0433\u043e \u0442\u0430\u043b\u043e\u043d\u0430')     ticket_id = Column(BIGINT, ForeignKey('bookings.tickets.ticket_id'), nullable=False, unique=True,                        comment='\u041d\u043e\u043c\u0435\u0440 \u0431\u0438\u043b\u0435\u0442\u0430')     seat_no = Column(String(4), nullable=False, comment='\u041d\u043e\u043c\u0435\u0440 \u043c\u0435\u0441\u0442\u0430')   TICKET_ROWS = [     {         \"ticket_id\": 5432000987,         \"passenger_id\": \"8149 604011\",         \"passenger_name\": \"VALERIY TIKHONOV\"     },     {         \"ticket_id\": 5432000988,         \"passenger_id\": \"8499 420203\",         \"passenger_name\": \"EVGENIYA ALEKSEEVA\"     },     {         \"ticket_id\": 5432000989,         \"passenger_id\": \"1011 752484\",         \"passenger_name\": \"ARTUR GERASIMOV\"     },     {         \"ticket_id\": 5432000990,         \"passenger_id\": \"4849 400049\",         \"passenger_name\": \"ALINA VOLKOVA\"     },     {         \"ticket_id\": 5432000991,         \"passenger_id\": \"6615 976589\",         \"passenger_name\": \"MAKSIM ZHUKOV\"     },     {         \"ticket_id\": 5432000992,         \"passenger_id\": \"2021 652719\",         \"passenger_name\": \"NIKOLAY EGOROV\"     },     {         \"ticket_id\": 5432000993,         \"passenger_id\": \"0817 363231\",         \"passenger_name\": \"TATYANA KUZNECOVA\"     },     {         \"ticket_id\": 5432000994,         \"passenger_id\": \"2883 989356\",         \"passenger_name\": \"IRINA ANTONOVA\"     },     {         \"ticket_id\": 5432000995,         \"passenger_id\": \"3097 995546\",         \"passenger_name\": \"VALENTINA KUZNECOVA\"     },     {         \"ticket_id\": 5432000996,         \"passenger_id\": \"6866 920231\",         \"passenger_name\": \"POLINA ZHURAVLEVA\"     },     {         \"ticket_id\": 5432000997,         \"passenger_id\": \"6030 369450\",         \"passenger_name\": \"ALEKSANDR TIKHONOV\"     },     {         \"ticket_id\": 5432000998,         \"passenger_id\": \"8675 588663\",         \"passenger_name\": \"ILYA POPOV\"     },     {         \"ticket_id\": 5432000999,         \"passenger_id\": \"0764 728785\",         \"passenger_name\": \"ALEKSANDR KUZNECOV\"     },     {         \"ticket_id\": 5432001000,         \"passenger_id\": \"8954 972101\",         \"passenger_name\": \"VSEVOLOD BORISOV\"     },     {         \"ticket_id\": 5432001001,         \"passenger_id\": \"6772 748756\",         \"passenger_name\": \"NATALYA ROMANOVA\"     },     {         \"ticket_id\": 5432001002,         \"passenger_id\": \"7364 216524\",         \"passenger_name\": \"ANTON BONDARENKO\"     },     {         \"ticket_id\": 5432001003,         \"passenger_id\": \"3635 182357\",         \"passenger_name\": \"VALENTINA NIKITINA\"     },     {         \"ticket_id\": 5432001004,         \"passenger_id\": \"8252 507584\",         \"passenger_name\": \"ALLA TARASOVA\"     },     {         \"ticket_id\": 5432001005,         \"passenger_id\": \"1026 982766\",         \"passenger_name\": \"OKSANA MOROZOVA\"     },     {         \"ticket_id\": 5432001006,         \"passenger_id\": \"7107 950192\",         \"passenger_name\": \"GENNADIY GERASIMOV\"     },     {         \"ticket_id\": 5432001007,         \"passenger_id\": \"4765 014996\",         \"passenger_name\": \"RAISA KONOVALOVA\"     } ]  TICKET_FLIGHTS_ROWS = [     {         \"ticket_id\": 5432000987,         \"flight_id\": 28935,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000988,         \"flight_id\": 28935,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000990,         \"flight_id\": 28939,         \"amount\": 18500.00     },     {         \"ticket_id\": 5432000989,         \"flight_id\": 28939,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000991,         \"flight_id\": 28913,         \"amount\": 18500.00     },     {         \"ticket_id\": 5432000992,         \"flight_id\": 28913,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000993,         \"flight_id\": 28913,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000994,         \"flight_id\": 28912,         \"amount\": 6800.00     },     {         \"ticket_id\": 5432000995,         \"flight_id\": 28912,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000996,         \"flight_id\": 28929,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000998,         \"flight_id\": 28904,         \"amount\": 18500.00     },     {         \"ticket_id\": 5432000999,         \"flight_id\": 28904,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432000997,         \"flight_id\": 28904,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432001001,         \"flight_id\": 28895,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432001000,         \"flight_id\": 28895,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432001002,         \"flight_id\": 28895,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432001003,         \"flight_id\": 28948,         \"amount\": 18500.00     },     {         \"ticket_id\": 5432001004,         \"flight_id\": 28948,         \"amount\": 6800.00     },     {         \"ticket_id\": 5432001005,         \"flight_id\": 28942,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432001007,         \"flight_id\": 28915,         \"amount\": 6200.00     },     {         \"ticket_id\": 5432001006,         \"flight_id\": 28915,         \"amount\": 6200.00     } ]  BOARDING_PASSES_ROWS = [     {         \"boarding_no\": 5432000959,         \"ticket_id\": 5432000997,         \"seat_no\": \"19F\"     },     {         \"boarding_no\": 5432000962,         \"ticket_id\": 5432000989,         \"seat_no\": \"18E\"     },     {         \"boarding_no\": 5432000963,         \"ticket_id\": 5432001005,         \"seat_no\": \"17C\"     },     {         \"boarding_no\": 5432000965,         \"ticket_id\": 5432001006,         \"seat_no\": \"16C\"     },     {         \"boarding_no\": 5432000969,         \"ticket_id\": 5432000995,         \"seat_no\": \"17A\"     },     {         \"boarding_no\": 5432000970,         \"ticket_id\": 5432000993,         \"seat_no\": \"19E\"     },     {         \"boarding_no\": 5432000974,         \"ticket_id\": 5432000988,         \"seat_no\": \"10E\"     },     {         \"boarding_no\": 5432000977,         \"ticket_id\": 5432000987,         \"seat_no\": \"7A\"     },     {         \"boarding_no\": 5432000978,         \"ticket_id\": 5432001002,         \"seat_no\": \"12C\"     },     {         \"boarding_no\": 5432000979,         \"ticket_id\": 5432001000,         \"seat_no\": \"11D\"     },     {         \"boarding_no\": 5432000981,         \"ticket_id\": 5432001001,         \"seat_no\": \"11A\"     },     {         \"boarding_no\": 5432000982,         \"ticket_id\": 5432001007,         \"seat_no\": \"8D\"     },     {         \"boarding_no\": 5432000983,         \"ticket_id\": 5432000999,         \"seat_no\": \"8F\"     },     {         \"boarding_no\": 5432000984,         \"ticket_id\": 5432000996,         \"seat_no\": \"14A\"     },     {         \"boarding_no\": 5432000986,         \"ticket_id\": 5432000994,         \"seat_no\": \"6F\"     },     {         \"boarding_no\": 5432000987,         \"ticket_id\": 5432000992,         \"seat_no\": \"5F\"     },     {         \"boarding_no\": 5432000988,         \"ticket_id\": 5432000990,         \"seat_no\": \"3F\"     },     {         \"boarding_no\": 5432000989,         \"ticket_id\": 5432001004,         \"seat_no\": \"6F\"     },     {         \"boarding_no\": 5432000990,         \"ticket_id\": 5432000991,         \"seat_no\": \"1D\"     },     {         \"boarding_no\": 5432000996,         \"ticket_id\": 5432000998,         \"seat_no\": \"2C\"     },     {         \"boarding_no\": 5432001000,         \"ticket_id\": 5432001003,         \"seat_no\": \"2C\"     } ]  BOOKINGS_ROWS = {     Ticket: TICKET_ROWS,     Ticket_Flights: TICKET_FLIGHTS_ROWS,     Boarding_Passes: BOARDING_PASSES_ROWS } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u0448\u0430\u0433\u043e\u043c \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043a\u043b\u0430\u0441\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u043f\u043e\u0434\u043d\u0438\u043c\u0430\u0442\u044c \u0438\u0437 \u043d\u0443\u0436\u043d\u043e\u0433\u043e \u043d\u0430\u043c Docker &#8212; \u043e\u0431\u0440\u0430\u0437\u0430  \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440, \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0432 \u043d\u0451\u043c \u0441\u0435\u0440\u0432\u0435\u0440 \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445, \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u0430\u043c\u0438 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043d\u0430\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u0438\u0445 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u0440\u0430\u043d\u0435\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0438 \u0432 \u043c\u043e\u0434\u0443\u043b\u044f\u0445 <strong>airline_db.py<\/strong> \u0438 <strong>bookings_db.py<\/strong><\/p>\n<p><strong>db_test.py<\/strong><\/p>\n<pre><code class=\"python\">from sqlalchemy import create_engine from sqlalchemy.dialects.postgresql import insert from testcontainers.postgres import PostgresContainer  from db_services.engine_factory import EngineFactory from .airline_db import Base as Airline_Base, AIRLINE_ROWS from .bookings_db import Base as Bookings_Base, BOOKINGS_ROWS   class MetaSingleton(type):     _instances = {}      def __call__(cls, *args, **kwargs):         if cls not in cls._instances:             cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)         return cls._instances[cls]   class TestBases(metaclass=MetaSingleton):     db = None     main_url = None      def __init__(self, db_image_name):         __engine = EngineFactory()         __engine.stand = 'localhost'         # \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0438\u0437 \u043e\u0431\u0440\u0430\u0437\u0430 DB_IMAGE         __postgres_container = PostgresContainer(image=db_image_name)         self.db = __postgres_container.start()         self.main_url = self.db.get_connection_url()          __BASES = {'airline': {'class': Airline_Base, 'rows': AIRLINE_ROWS},                    'bookings': {'class': Bookings_Base, 'rows': BOOKINGS_ROWS}                    }          # \u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0431\u0430\u0437, \u0441\u0445\u0435\u043c, \u043d\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u043c\u0438         for __base_name, __base_data in __BASES.items():             self.create_base(base_name=__base_name)             __engine.user, __engine.passw = 'test', 'test'             __url = __engine.get_postgres_url(base_name=__base_name)             self.create_schema(schema_name=__base_name, url=__url)             __db_engine = __engine.get_engine(__base_name)             __base_data.get('class').metadata.create_all(__db_engine)              for __cls, __rows in __base_data.get('rows').items():                 __db_engine.execute(insert(__cls).values(__rows))      def create_base(self, base_name):         __engine = create_engine(self.main_url)         __connection = __engine.connect()         __connection.execution_options(isolation_level='AUTOCOMMIT').execute(f'create database {base_name}')         __host, __port = self.main_url.replace('postgresql+psycopg2:\/\/test:test@', '').replace('\/test', '').split(':')         __new_base_url = f'postgresql+psycopg2:\/\/test:test@{__host}:{__port}\/{base_name}'         #\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 \u043d\u043e\u0432\u043e\u0439 \u0431\u0430\u0437\u043e\u0439 \u0432 EngineFactory         __engine = EngineFactory()         __engine.add_db(base_name=base_name, url=__new_base_url)      def create_schema(self, url, schema_name):         __engine = create_engine(url)         __connection = __engine.connect()         __connection.execution_options(isolation_level='AUTOCOMMIT').execute(f'create schema {schema_name}') <\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0442\u0430\u043a\u0436\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c singleton, \u0442.\u043a. \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0447\u0442\u043e\u0431\u044b \u0443 \u043d\u0430\u0441 \u043f\u043e\u0434\u043d\u0438\u043c\u0430\u043b\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d testcontainers. \u041e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u0434\u043e\u043f\u0438\u0441\u0430\u0442\u044c \u0441\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u044b:<\/p>\n<p><strong>tests.py<\/strong><\/p>\n<pre><code class=\"python\">import pytest  from processing.airline import set_flight_status, get_flight_status from processing.bookings import get_premium_psg_list from .db_test import TestBases   @pytest.fixture(scope=\"session\", autouse=True) def test_db():     # \u042d\u0442\u043e\u0442 \u0431\u043b\u043e\u043a \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u0442\u0435\u0441\u0442\u043e\u0432     test_base = TestBases(db_image_name='postgres:11.8')     yield     # \u042d\u0442\u043e\u0442 \u0431\u043b\u043e\u043a \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d \u043f\u043e\u0441\u043b\u0435 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0435\u0441\u0442\u043e\u0432     test_base.db.stop()   # \u0422\u0435\u0441\u0442 \u043c\u0435\u0442\u043e\u0434\u0430 processing.bookings.get_premium_psg_list() # \u0412 \u0442\u0435\u043a\u0443\u0449\u0438\u0445 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445, \u0434\u043b\u044f limit=10000, \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 == 4 def test_get_premium_psg_list(test_db):     assert len(get_premium_psg_list(limit=10000)) == 4   # \u0422\u0435\u0441\u0442 \u043c\u0435\u0442\u043e\u0434\u0430 processing.airline.get_flight_status() # \u0414\u043b\u044f flight_id=33043 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 'On Time' def test_get_flight_status_before(test_db):     assert get_flight_status(flight_id=33043) == [['On Time']]   # \u0422\u0435\u0441\u0442 \u043c\u0435\u0442\u043e\u0434\u0430 processing.airline.set_flight_status() # \u0412 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 airline.flights \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0441 flight_id=33043, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 - 1 # !!!\u0422\u0435\u0441\u0442 \u043c\u0435\u043d\u044f\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b!!! def test_set_flight_status(test_db):     assert set_flight_status(flight_id=33043, status='Delayed') == 1   # \u0422\u0435\u0441\u0442 \u043c\u0435\u0442\u043e\u0434\u0430 processing.airline.get_flight_status() # \u041f\u043e\u0441\u043b\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u0430 test_set_flight_status(test_db) \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u043e\u0441\u044c. # \u041a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0442\u0435\u0441\u0442\u0430 \u0434\u043b\u044f flight_id=33043 - 'Delayed' def test_get_flight_status_after(test_db):     assert get_flight_status(flight_id=33043) == [['Delayed']]   # \u0442\u0435\u0441\u0442, \u0434\u043b\u044f \u0441\u043b\u0443\u0447\u0430\u044f \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0434\u043e\u043a\u0435\u0440-\u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0435\u0441\u0442\u043e\u0432 # def test_debug(test_db): #     while True: #         pass <\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u043c\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0438 4 \u0442\u0435\u0441\u0442\u0430, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0438 \u0431\u0443\u0434\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435. \u041d\u043e \u0447\u0442\u043e \u0431\u043e\u043b\u0435\u0435 \u0432\u0430\u0436\u043d\u043e, \u0437\u0434\u0435\u0441\u044c \u043c\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u043b\u0438 \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0443 <code>test_db()<\/code>, \u0432\u043d\u0443\u0442\u0440\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b. \u0422\u0435\u0441\u0442\u043e\u0432\u0443\u044e \u0441\u0440\u0435\u0434\u0443 pytest \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u0434 \u043a\u0430\u0436\u0434\u044b\u043c \u0442\u0435\u0441\u0442\u043e\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0435\u0451 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442, \u043d\u043e \u0442.\u043a. \u043c\u044b \u0443\u043a\u0430\u0437\u0430\u043b\u0438 <code>scope=\"session\"<\/code>, \u0442\u043e \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0434\u043b\u044f \u0432\u0441\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432. \u0418 \u0435\u0441\u043b\u0438 \u043a\u0430\u043a\u043e\u0439-\u043b\u0438\u0431\u043e \u0438\u0437 \u0442\u0435\u0441\u0442\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0411\u0414, \u0442\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0442\u0435\u0441\u0442 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u043e\u0439 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b. \u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0442\u044c. \u0412 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438, \u044d\u0442\u043e\u0442 \u043f\u0440\u0438\u043d\u0446\u0438\u043f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u043d\u0430\u0448\u0438\u0445 \u043f\u0440\u0438\u043c\u0435\u0440\u0430\u0445.<\/p>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0442\u0435\u0441\u0442\u044b \u0438 \u0440\u0430\u0434\u0443\u0435\u043c\u0441\u044f \u0437\u0435\u043b\u0451\u043d\u044b\u043c\u0438 \u0433\u0430\u043b\u043e\u0447\u043a\u0430\u043c\u0438 \u0432 Test Result \ud83d\ude42<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/xt\/dj\/yo\/xtdjyozs9zxwobu96cjknio797a.png\" width=\"auto\" height=\"auto\" data-src=\"https:\/\/habrastorage.org\/webt\/xt\/dj\/yo\/xtdjyozs9zxwobu96cjknio797a.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u0442\u0435\u0441\u0442\u043e\u0432, testcontainers \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442 \u0440\u0430\u0431\u043e\u0442\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0438 \u0443\u0434\u0430\u043b\u0438\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u0411\u044b\u0432\u0430\u0435\u0442 \u043f\u043e\u043b\u0435\u0437\u043d\u043e &#171;\u043f\u0440\u0438\u0434\u0435\u0440\u0436\u0430\u0442\u044c&#187; \u0442\u0435\u0441\u0442\u043e\u0432\u0443\u044e \u0411\u0414 \u043d\u0430 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0432\u0440\u0435\u043c\u044f, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0437\u0430\u043b\u0435\u0437\u0442\u044c \u0432 \u0411\u0414 \u0438\u0437 \u043e\u0431\u044b\u0447\u043d\u043e\u0439 IDE, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0430\u0440\u0443-\u0442\u0440\u043e\u0439\u043a\u0443 SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0440\u0430\u0441\u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442 <code>test_debug(test_db)<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f\u0441\u044c \u0432 \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e\u043c \u0446\u0438\u043a\u043b\u0435, \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0411\u0414 \u043f\u043e\u0434 \u043b\u043e\u0433\u0438\u043d\u043e\u043c <code>test<\/code> \u0438 \u043f\u0430\u0440\u043e\u043b\u0435\u043c <code>test<\/code>. \u041f\u043e\u0440\u0442 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 Docker Desktop<\/p>\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/webt\/hy\/qz\/fe\/hyqzfem0pjkfeu3xhxokiuo9qee.png\" width=\"auto\" height=\"auto\" data-src=\"https:\/\/habrastorage.org\/webt\/hy\/qz\/fe\/hyqzfem0pjkfeu3xhxokiuo9qee.png\"\/><figcaption><\/figcaption><\/figure>\n<p>\u043b\u0438\u0431\u043e \u0438\u0437 \u043a\u043e\u043d\u0441\u043e\u043b\u0438:<\/p>\n<pre><code class=\"powershell\"># \u0421\u043f\u0438\u0441\u043e\u043a \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u043e\u0432: docker ps                                                               CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS                     NAMES 46fb9a865f58   postgres:11.8   \"docker-entrypoint.s\u2026\"   13 minutes ago   Up 13 minutes   0.0.0.0:56517->5432\/tcp   clever_einstein   # \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u043f\u043e\u0440\u0442\u044b \u043d\u0443\u0436\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430  docker port 46fb9a865f58                                                5432\/tcp -> 0.0.0.0:56517<\/code><\/pre>\n<h3>\u0418\u0442\u043e\u0433\u0438<\/h3>\n<p>\u041c\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u0441\u043e\u0437\u0434\u0430\u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0438 \u0411\u0414 \u0441\u043b\u043e\u0439 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e testcontainers \u0438 pytest. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u0435\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0411\u0414 \u0438\u043b\u0438 \u043d\u0430 \u0435\u0451 \u0440\u0435\u043f\u043b\u0438\u043a\u0435, \u0442\u043e \u0441\u043c\u044b\u0441\u043b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f testcontainers \u0442\u0435\u0440\u044f\u0435\u0442\u0441\u044f, \u0430 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0431\u0430\u0437 \u0438 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u043d\u0435\u043d\u0443\u0436\u043d\u043e\u0439 \u0442\u0440\u0430\u0442\u043e\u0439 \u0440\u0430\u0431\u043e\u0447\u0435\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438. \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043e\u0439 testcontainers \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0411\u0414 \u0441 \u043d\u0443\u0436\u043d\u044b\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c\u0438. \u041d\u043e \u0435\u0441\u043b\u0438 \u043d\u0438\u0447\u0435\u0433\u043e \u0442\u0430\u043a\u043e\u0433\u043e \u043f\u043e\u0434 \u0440\u0443\u043a\u043e\u0439 \u043d\u0435\u0442, \u0430 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c, testcontainers \u0432\u043f\u043e\u043b\u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u044b\u0445\u043e\u0434\u043e\u043c \u0432 \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438.<\/p>\n<p>\u0421\u043a\u0430\u0447\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u043c\u043e\u0436\u043d\u043e \u0441 \u043c\u043e\u0435\u0433\u043e <a href=\"https:\/\/github.com\/avdavydov\/db_tests_example.git\" rel=\"noopener noreferrer nofollow\">\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f<\/a> GitHub.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/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\/post\/677598\/\"> https:\/\/habr.com\/ru\/post\/677598\/<\/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<figure class=\"\"><figcaption><\/figcaption><\/figure>\n<p>\u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u0431\u043e\u043b\u044c\u0448\u0443\u044e \u043f\u043e\u043f\u0443\u043b\u044f\u0440\u043d\u043e\u0441\u0442\u044c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 <code>testcontainers<\/code> \u0432 \u043c\u0438\u0440\u0435 java, \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432 \u0441\u0435\u0442\u0438 \u043f\u043e  \u0435\u0451 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044e \u0432 python \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043d\u0435\u0442. \u0414\u0430\u043d\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f &#8212; \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043b\u0438\u043a\u0432\u0438\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u043f\u0440\u043e\u0431\u0435\u043b. \u042f \u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e pytest \u0438 testcontainers, \u0447\u0442\u043e \u044d\u0442\u043e \u0442\u0430\u043a\u043e\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0435, \u044f \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u043a\u0430\u0436\u0443 \u043f\u0440\u0438\u043c\u0435\u0440 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u044d\u0442\u043e \u0432\u043e\u0435\u0434\u0438\u043d\u043e. <\/p>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0411\u0414 \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PostgreSQL. \u0412 \u0441\u0435\u0442\u0438 \u0435\u0441\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 <a href=\"https:\/\/readthedocs.org\/projects\/testcontainers-python\/downloads\/pdf\/stable\/\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440<\/a> \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f testcontainers \u0441 PostgreSQL:<\/p>\n<pre><code class=\"python\">with PostgresContainer(\"postgres:9.5\") as postgres:   e = sqlalchemy.create_engine(postgres.get_connection_url())   result = e.execute(\"select version()\")<\/code><\/pre>\n<p>\u0414\u0430, \u043d\u0435 \u043c\u043d\u043e\u0433\u043e. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0437\u043e\u0432\u044c\u0451\u043c \u044d\u0442\u043e\u0442 \u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438. <\/p>\n<h3>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h3>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u0430, \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0447\u0430\u0441\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0432 \u0434\u0432\u0443\u0445 \u0411\u0414 \u0438 \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c \u0435\u0433\u043e. \u0412 \u043d\u0430\u0448\u0435\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u043c\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0442\u0440\u0438 \u043c\u0435\u0442\u043e\u0434\u0430 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u0411\u0414, \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u0438 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0411\u0414, \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0441\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u044b \u0438 \u0432\u0441\u0451 \u0442\u043e, \u0447\u0442\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438 \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0439 \u0441\u0440\u0435\u0434\u044b. \u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<figure class=\"\"><figcaption><\/figcaption><\/figure>\n<p>\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u0435 \u043f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u044f \u043a \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0435:<\/p>\n<ul>\n<li>\n<p><strong>db_services<\/strong> &#8212; \u043f\u0430\u043a\u0435\u0442, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0440\u0430\u0441\u043f\u043e\u043b\u0430\u0433\u0430\u044e\u0442\u0441\u044f \u0431\u0430\u0437\u043e\u0432\u044b\u0435 \u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0411\u0414<\/p>\n<\/li>\n<li>\n<p><strong>processing<\/strong> &#8212; \u043f\u0430\u043a\u0435\u0442, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/p>\n<\/li>\n<li>\n<p><strong>tests<\/strong> &#8212; \u043f\u0430\u043a\u0435\u0442, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u0432\u0441\u0451 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0442\u0435\u0441\u0442\u043e\u0432\u043e\u0433\u043e SQL &#8212; \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438 \u043d\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u0434\u0430\u043d\u043d\u044b\u043c\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u0430\u043c\u0438 \u0442\u0435\u0441\u0442\u044b.<\/p>\n<p>\u0417\u0430 \u043e\u0441\u043d\u043e\u0432\u0443 \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u043e\u0437\u044c\u043c\u0451\u043c \u043a\u0443\u0441\u043e\u0447\u0435\u043a \u043e\u0442 <a href=\"https:\/\/postgrespro.ru\/education\/demodb\" rel=\"noopener noreferrer nofollow\">\u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445<\/a> \u043e\u0442 PostgresPro \u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0431\u0430\u0437\u044b:<\/p>\n<\/li>\n<\/ul>\n<figure class=\"\"><figcaption>\u0411\u0414 Airline<\/figcaption><\/figure>\n<figure class=\"\"><figcaption>\u0411\u0414 Bookings<\/figcaption><\/figure>\n<h3>\u041c\u043e\u0434\u0443\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h3>\n<p>\u0414\u0430\u0431\u044b \u043d\u0435 \u043f\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0430\u0442\u044c \u043f\u043e\u0441\u0442 \u043a\u043e\u0434\u043e\u043c, \u0443\u0431\u0435\u0440\u0443 \u043c\u043e\u0434\u0443\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u0434 \u0441\u043f\u043e\u0439\u043b\u0435\u0440.<\/p>\n<details class=\"spoiler\">\n<summary>\u041c\u043e\u0434\u0443\u043b\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/summary>\n<div class=\"spoiler__content\">\n<p><strong>engine_factory.py<br \/><\/strong>\u0412 \u043c\u043e\u0434\u0443\u043b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0444\u0430\u0431\u0440\u0438\u043a\u0430 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 \u043a\u043e \u0432\u0441\u0435\u043c \u0411\u0414 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u043f\u043e \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u0443 <em>singleton. <\/em>\u0422\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0434\u043d\u0438 \u0438 \u0442\u0435 \u0436\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043a \u0411\u0414 \u0438\u0437 \u043b\u044e\u0431\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442\u043d\u044b\u0445 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0439 \u0432 \u0432\u0438\u0434\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439.<\/p>\n<pre><code class=\"python\">import sqlalchemy.engine from sqlalchemy import create_engine   class MetaSingleton(type):     _instances = {}      def __call__(cls, *args, **kwargs):         if cls not in cls._instances:             cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)         return cls._instances[cls]   class EngineFactory(metaclass=MetaSingleton):     connections, db_urls = ({},) * 2     user, passw, stand, db_name = (None,) * 4      def get_engine(self, db_name, schema_name=None) -> sqlalchemy.engine.Engine:         self.db_name = db_name          if None in (self.user, self.stand, self.db_name):             raise ValueError('\u041d\u0435 \u0437\u0430\u0434\u0430\u043d\u044b \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b: stand, user, db_name')          if self.connections.get((db_name, schema_name)):             return self.connections.get((db_name, schema_name))         else:             url = self.get_postgres_url(db_name)             if schema_name:                 self.connections[(db_name, schema_name)] = create_engine(url, echo=False, echo_pool=False,                                                                          connect_args={                                                                              'options': f'-csearch_path={schema_name}'})             else:                 self.connections[(db_name, schema_name)] = create_engine(url, echo=False, echo_pool=False)         return self.connections[(db_name, schema_name)]      def dispose_engines(self) -> None:         for engine in self.connections.values():             engine.dispose()         self.connections = {}      def add_db(self, base_name, url):         self.db_urls[base_name] = url      def get_postgres_url(self, base_name) -> str:         stand = self.stand.lower()          if stand == 'localhost':             return self.db_urls.get(base_name) if self.db_urls.get(base_name) else ValueError(                 f'''URL \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 stand={stand}, db_name='{base_name}' \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d ''') <\/code><\/pre>\n<p><strong>db_service.py<\/strong><br \/>\u0417\u0434\u0435\u0441\u044c \u043d\u0430\u043f\u0438\u0448\u0435\u043c \u0434\u0432\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u0434\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 \u0411\u0414 (GET \u0438 POST), \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0443\u043f\u0440\u043e\u0441\u0442\u044f\u0442 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a \u0411\u0414 \u0447\u0435\u0440\u0435\u0437 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439.<\/p>\n<pre><code class=\"python\">from .engine_factory import EngineFactory engine = EngineFactory()   def get_from_postgres(sql, db_name, schema_name=None) -> list:     result = []     pg_engine = engine.get_engine(db_name=db_name, schema_name=schema_name)     try:         with pg_engine.connect() as connection:             cursor = connection.execution_options(stream_result=True).execute(sql)             for row in cursor:                 result.append(list(row))     except Exception as e:         raise RuntimeError(f'\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438 \u043a \u0411\u0414: {e}')     return result   def post_to_postgres(sql, db_name, schema_name=None) -> int:     pg_engine = engine.get_engine(db_name=db_name, schema_name=schema_name)     rows_processed = 0     try:         with pg_engine.connect() as connection:             cursor = connection.execution_options(stream_result=True, isolation_level='AUTOCOMMIT').execute(sql)             rows_processed = cursor.rowcount             cursor.close()     except Exception as e:         raise RuntimeError(f'\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 {sql} \u0432 \u0411\u0414: {e}')     return rows_processed<\/code><\/pre>\n<p><strong>airline.py<\/strong><br \/>\u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u043c\u043e\u0434\u0443\u043b\u0435 \u043e\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434\u044b, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0432 \u0411\u0414 <em>Airline<\/em><\/p>\n<pre><code class=\"python\">from db_services.db_service import get_from_postgres from db_services.db_service import post_to_postgres  DB_NAME = 'airline'  # \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u0440\u0435\u0439\u0441\u0430 def set_flight_status(flight_id, status) -> int:     sql = '''     update airline.flights         set status = '%s'         where flight_id = %d     ''' % (status, flight_id)      try:         return post_to_postgres(sql=sql, db_name=DB_NAME)     except Exception as e:         raise RuntimeError(e)  # \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0440\u0435\u0439\u0441\u0430 def get_flight_status(flight_id) -> list:     sql = '''     select status         from airline.flights         where flight_id = %d     ''' % flight_id      try:         return get_from_postgres(sql=sql, db_name=DB_NAME)     except Exception as e:         raise RuntimeError(e)<\/code><\/pre>\n<p><strong>bookings.py<\/strong><br \/>\u0412 \u0434\u0430\u043d\u043d\u043e\u043c \u043c\u043e\u0434\u0443\u043b\u0435 \u043e\u043f\u0438\u0448\u0435\u043c \u043c\u0435\u0442\u043e\u0434\u044b, \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0449\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0432 \u0411\u0414 <em>Bookings<\/em><\/p>\n<pre><code class=\"python\">from db_services.db_service import get_from_postgres  DB_NAME = 'bookings'  # \u041f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0430\u0441\u0441\u0430\u0436\u0438\u0440\u043e\u0432, \u0442\u0440\u0430\u0442\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u043e\u043b\u0435\u0435 limit def get_premium_psg_list(limit) -> list:     sql = '''             select passenger_name, sum(amount)                     from bookings.tickets                              join bookings.ticket_flights using (ticket_id)                     group by 1                     having sum(amount) > %d ''' % limit      try:         return get_from_postgres(sql=sql, db_name=DB_NAME)     except Exception as e:         raise RuntimeError(e) <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h3>\u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0411\u0414<\/h3>\n<p>\u041e\u0447\u0435\u0432\u0438\u0434\u043d\u043e, \u0447\u0442\u043e \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0435\u043c\u0430\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u0430\u044f \u0411\u0414 \u0434\u043e\u043b\u0436\u043d\u0430 \u043e\u0442\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432\u043e\u0439 \u0411\u0414, \u043f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u043e\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0414\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0445 \u0411\u0414, \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c ORM \u0441 \u0434\u0435\u043a\u043b\u0430\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u043c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043a\u043b\u0430\u0441\u0441\u043e\u0432.<\/p>\n<h4>\u0421\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u0438 \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0411\u0414 Airline<\/h4>\n<details class=\"spoiler\">\n<summary>airline_db.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">from sqlalchemy import Column, String, INTEGER, TEXT, ForeignKey from sqlalchemy.ext.declarative import declarative_base  Base = declarative_base()   class Aircrafts(Base):     __tablename__ = 'aircrafts'     __table_args__ = {'schema': 'airline'}      aircraft_code = Column(String(3), nullable=False, primary_key=True, comment='\u041a\u043e\u0434 \u0441\u0430\u043c\u043e\u043b\u0435\u0442\u0430, IATA')     model = Column(TEXT, nullable=False, comment='\u041c\u043e\u0434\u0435\u043b\u044c \u0441\u0430\u043c\u043e\u043b\u0435\u0442\u0430')     range = Column(INTEGER, nullable=False, comment='\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0434\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043f\u043e\u043b\u0435\u0442\u0430, \u043a\u043c')   class Flights(Base):     __tablename__ = 'flights'     __table_args__ = {'schema': 'airline'}      flight_id = Column(INTEGER, nullable=False, primary_key=True, comment='\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0440\u0435\u0439\u0441\u0430')     flight_no = Column(String(10), nullable=False, comment='\u041d\u043e\u043c\u0435\u0440 \u0440\u0435\u0439\u0441\u0430')     aircraft_code = Column(String(3), ForeignKey('airline.aircrafts.aircraft_code'), nullable=False,                            comment='\u041a\u043e\u0434 \u0441\u0430\u043c\u043e\u043b\u0435\u0442\u0430, IATA')     status = Column(String(20), nullable=False, comment='\u0421\u0442\u0430\u0442\u0443\u0441 \u0440\u0435\u0439\u0441\u0430')   AIRCRAFTS_ROWS = [     {         \"aircraft_code\": \"773\",         \"model\": \"Boeing 777-300\",         \"range\": 11100     },     {         \"aircraft_code\": \"763\",         \"model\": \"Boeing 767-300\",         \"range\": 7900     },     {         \"aircraft_code\": \"SU9\",         \"model\": \"Sukhoi Superjet-100\",         \"range\": 3000     },     {         \"aircraft_code\": \"320\",         \"model\": \"Airbus A320-200\",         \"range\": 5700     },     {         \"aircraft_code\": \"321\",         \"model\": \"Airbus A321-200\",         \"range\": 5600     },     {         \"aircraft_code\": \"319\",         \"model\": \"Airbus A319-100\",         \"range\": 6700     },     {         \"aircraft_code\": \"733\",         \"model\": \"Boeing 737-300\",         \"range\": 4200     },     {         \"aircraft_code\": \"CN1\",         \"model\": \"Cessna 208 Caravan\",         \"range\": 1200     },     {         \"aircraft_code\": \"CR2\",         \"model\": \"Bombardier CRJ-200\",         \"range\": 2700     } ]  FLIGHTS_ROWS = [     {         \"flight_id\": 32959,         \"flight_no\": \"PG0550\",         \"aircraft_code\": \"CR2\",         \"status\": \"On Time\"     },     {         \"flight_id\": 28948,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33116,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"On Time\"     },     {         \"flight_id\": 33117,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33111,         \"flight_no\": \"PG0063\",         \"aircraft_code\": \"CR2\",         \"status\": \"Scheduled\"     },     {         \"flight_id\": 28929,         \"flight_no\": \"PG0242\",         \"aircraft_code\": \"SU9\",         \"status\": \"Arrived\"     },     {         \"flight_id\": 33052,         \"flight_no\": \"PG0359\",         \"aircraft_code\": \"CR2\",         \"status\": \"Cancelled\"     },     {         \"flight_id\": 33043,         \"flight_no\": \"PG0359\",         \"aircraft_code\": \"CR2\",    <\/code><\/pre>\n<\/div>\n<\/details>\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-335864","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/335864","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=335864"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/335864\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=335864"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=335864"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=335864"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}