{"id":456435,"date":"2025-04-17T21:00:20","date_gmt":"2025-04-17T21:00:20","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=456435"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=456435","title":{"rendered":"<span>LitestarCatsCV. \u0422\u0440\u0435\u043d\u0438\u0440\u0443\u0435\u043c\u0441\u044f \u043d\u0430 \u043a\u043e\u0448\u043a\u0430\u0445. \u0420\u0430\u0441\u0448\u0438\u0440\u044f\u0435\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0438 \u0433\u043e\u0442\u043e\u0432\u0438\u043c\u0441\u044f \u043a \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0443. \u0427\u0430\u0441\u0442\u044c 3<\/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<h3>\u041a\u043e\u0442\u0438\u043a\u0438 \u0432\u044b\u0445\u043e\u0434\u044f\u0442 \u043d\u0430 \u043d\u043e\u0432\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c! \ud83d\udc3e<\/h3>\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u043a\u043e\u0442\u0438\u043a\u0438 \u0438 \u043a\u043e\u0442\u043e\u043b\u044e\u0431\u044b! \u0412 <a href=\"https:\/\/habr.com\/ru\/companies\/ntechlab\/articles\/883578\/\">\u043f\u0435\u0440\u0432\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/a> \u043d\u0430\u0448\u0435\u0433\u043e \u043a\u043e\u0448\u0430\u0447\u044c\u0435\u0433\u043e \u043f\u0440\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b (Litestar \u0432\u043c\u0435\u0441\u0442\u043e FastAPI, Granian \u0432\u043c\u0435\u0441\u0442\u043e Gunicorn, KeyDB \u0432\u043c\u0435\u0441\u0442\u043e Redis), \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043b\u0438 uv \u0438 \u0437\u0430\u043b\u043e\u0436\u0438\u043b\u0438 \u0444\u0443\u043d\u0434\u0430\u043c\u0435\u043d\u0442 \u043f\u0440\u043e\u0435\u043a\u0442\u0430. <strong>\u0412\u043e <\/strong><a href=\"https:\/\/habr.com\/ru\/companies\/ntechlab\/articles\/889022\/\"><code><strong>\u0432\u0442\u043e\u0440\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/strong><\/code><\/a><strong> <\/strong>\u043c\u044b \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u043b\u0438 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u043e\u0435 CRUD API \u0434\u043b\u044f \u0440\u0435\u0437\u044e\u043c\u0435 \u043a\u043e\u0442\u0438\u043a\u043e\u0432 (\u0438\u043b\u0438 \u043b\u044e\u0434\u0435\u0439, \u0435\u0441\u043b\u0438 \u0432\u0430\u043c \u0442\u0430\u043a \u0431\u043b\u0438\u0436\u0435), \u043f\u043e\u0434\u0440\u0443\u0436\u0438\u043b\u0438 \u0435\u0433\u043e \u0441 PostgreSQL \u0447\u0435\u0440\u0435\u0437 SQLAlchemy, \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043b\u0438 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Alembic \u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0442\u0435\u0441\u0442\u044b \u0441 Pytest. \u0423 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u0441\u0442\u0435\u043d\u044b \u0438 \u0444\u0443\u043d\u0434\u0430\u043c\u0435\u043d\u0442, \u043d\u043e \u043f\u043e\u0440\u0430 \u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043a\u0440\u044b\u0448\u0443 \u0438 \u0433\u043e\u0442\u043e\u0432\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0443! \ud83c\udfe0<\/p>\n<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u043c\u044b \u0441\u0434\u0435\u043b\u0430\u0435\u043c \u043d\u0430\u0448 API \u0435\u0449\u0451 \u043a\u0440\u0443\u0447\u0435: \u0432\u044b\u043d\u0435\u0441\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e msgspec, \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 JWT \u0432 Litestar, \u0443\u0441\u043a\u043e\u0440\u0438\u043c API \u0441 KeyDB, \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043c \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0441 coverage, \u0443\u043f\u0430\u043a\u0443\u0435\u043c \u0432\u0441\u0451 \u0432 Docker \u0438 \u043d\u0430\u0440\u0438\u0441\u0443\u0435\u043c \u0440\u0435\u0437\u044e\u043c\u0435 \u043a\u043e\u0442\u0438\u043a\u043e\u0432 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Jinja. \u041a \u043a\u043e\u043d\u0446\u0443 \u0441\u0442\u0430\u0442\u044c\u0438 \u043d\u0430\u0448 \u043a\u043e\u0448\u0430\u0447\u0438\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432 \u043a \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0436\u0438\u0437\u043d\u0438 \u2014 \u043f\u043e\u0435\u0445\u0430\u043b\u0438! \ud83d\ude80<\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9ea\/233\/c75\/9ea233c7582d6383b9b75ca6c61b24f5.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/9ea\/233\/c75\/9ea233c7582d6383b9b75ca6c61b24f5.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9ea\/233\/c75\/9ea233c7582d6383b9b75ca6c61b24f5.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<hr\/>\n<p>\u0415\u0441\u043b\u0438 \u0440\u0430\u043d\u044c\u0448\u0435 \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u0442\u0440\u0435\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u043d\u0430 \u043a\u043e\u0448\u043a\u0430\u0445, \u0442\u043e \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u044b\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0438\u0445 \u0432 \u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u043c\u0438\u0440 \u0441 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043c\u0438, \u043a\u0435\u0448\u0435\u043c \u0438 \u0441\u0442\u0438\u043b\u044c\u043d\u044b\u043c\u0438 \u0440\u0435\u0437\u044e\u043c\u0435. \u041f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u0434, \u043a\u0430\u043a \u0432\u0441\u0435\u0433\u0434\u0430, \u043d\u0430 GitHub \u2014 \u0441\u0441\u044b\u043b\u043a\u0430 \u0432 \u043a\u043e\u043d\u0446\u0435! \ud83d\udc31<\/p>\n<h3>\u0412\u044b\u043d\u043e\u0441 \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432 \u2014 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u043c \u0432\u0441\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432 \u043f\u043e\u0440\u044f\u0434\u043e\u043a \ud83d\uddc2\ufe0f<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/62c\/41e\/aed\/62c41eaed800a3c114f88583bb76dfdf.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/62c\/41e\/aed\/62c41eaed800a3c114f88583bb76dfdf.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/62c\/41e\/aed\/62c41eaed800a3c114f88583bb76dfdf.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u041d\u0430\u0448 \u043f\u0440\u043e\u0435\u043a\u0442 \u0440\u0430\u0441\u0442\u0451\u0442, \u0438 \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u0440\u043e\u0434\u0435 DATABASE_URL \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0442 \u043c\u044f\u0443\u043a\u0430\u0442\u044c \u043e\u0442 \u043d\u0435\u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430. \u0414\u0430\u0432\u0430\u0439 \u0432\u044b\u043d\u0435\u0441\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c src\/configs\/app_<a href=\"http:\/\/config.py\">config.py<\/a> \u0438 \u0431\u0443\u0434\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 settings-example.yaml. \u041c\u044b \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c msgspec \u0434\u043b\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439, \u0442\u0430\u043a \u0447\u0442\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c \u0435\u0433\u043e \u0438 \u0434\u043b\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432 \u2014 \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0435\u0442 \u043a\u043e\u0434 \u0447\u0438\u0449\u0435 \u0438 \u0431\u044b\u0441\u0442\u0440\u0435\u0435, \u0430 \u043a\u043e\u0442\u0438\u043a\u0438 \u043b\u044e\u0431\u044f\u0442 \u043f\u043e\u0440\u044f\u0434\u043e\u043a! \ud83d\ude3a<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e msgspec.Struct.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u043c \u043f\u0430\u0440\u0441\u0438\u043d\u0433 settings-example.yaml \u0432 \u044d\u0442\u0438 \u043a\u043b\u0430\u0441\u0441\u044b.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c src\/<a href=\"http:\/\/app.py\">app.py<\/a>, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0438.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439<\/h4>\n<p>\u041d\u0430\u043c \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f pyyaml \u0434\u043b\u044f \u043f\u0430\u0440\u0441\u0438\u043d\u0433\u0430 YAML. msgspec \u0443 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435:<\/p>\n<p><code>uv add pyyaml<\/code><\/p>\n<h4>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u0444\u0430\u0439\u043b src\/configs\/app_<a href=\"http:\/\/config.py\">config.py<\/a>. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c msgspec.Struct \u0434\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432:<\/p>\n<pre><code class=\"python\">from msgspec import Struct  import argparse  import yaml  from pathlib import Path  class DatabaseConfig(Struct):  \u00a0\u00a0\u00a0\u00a0user: str = \"postgres\"  \u00a0\u00a0\u00a0\u00a0password: str = \"postgres\"  \u00a0\u00a0\u00a0\u00a0host: str = \"127.0.0.1\"  \u00a0\u00a0\u00a0\u00a0port: str = \"5432\"  \u00a0\u00a0\u00a0\u00a0database_name: str = \"test_db\"  \u00a0\u00a0\u00a0\u00a0url: str = None  \u00a0\u00a0\u00a0\u00a0echo: bool = False  \u00a0\u00a0\u00a0\u00a0def get_connection_url(self) -&gt; str:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if self.url:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return self.url  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return f\"postgresql+asyncpg:\/\/{self.user}:{self.password}@{self.host}:{self.port}\/{self.database_name}\"  class KeyDBConfig(Struct):  \u00a0\u00a0\u00a0\u00a0host: str = \"127.0.0.1\"  \u00a0\u00a0\u00a0\u00a0port: str = \"6379\"  \u00a0\u00a0\u00a0\u00a0db: str = \"0\"  \u00a0\u00a0\u00a0\u00a0url: str = None  \u00a0\u00a0\u00a0\u00a0def get_connection_url(self) -&gt; str:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if self.url:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return self.url  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return f\"keydb:\/\/{self.host}:{self.port}\/{self.db}\"  class JWTConfig(Struct):  \u00a0\u00a0\u00a0\u00a0secret: str  \u00a0\u00a0\u00a0\u00a0token_secret: str  class AppConfig(Struct):  \u00a0\u00a0\u00a0\u00a0database: DatabaseConfig  \u00a0\u00a0\u00a0\u00a0keydb: KeyDBConfig  \u00a0\u00a0\u00a0\u00a0jwt: JWTConfig  def load_config(file_path: str) -&gt; AppConfig:  \u00a0\u00a0\u00a0\u00a0with open(file_path, \"r\") as f:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0config_data = yaml.safe_load(f)  \u00a0\u00a0\u00a0\u00a0return AppConfig(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0database=DatabaseConfig(**config_data[\"database\"]),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0keydb=KeyDBConfig(**config_data[\"keydb\"]),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0jwt=JWTConfig(**config_data[\"jwt\"]),  \u00a0\u00a0\u00a0\u00a0)  def configure() -&gt; AppConfig:  \u00a0\u00a0\u00a0\u00a0title = \"LitestarCats\"  \u00a0\u00a0\u00a0\u00a0parser = argparse.ArgumentParser(title)  \u00a0\u00a0\u00a0\u00a0src_dir = Path(__file__).absolute().parent  \u00a0\u00a0\u00a0\u00a0config_file = src_dir \/ \"settings-example.yaml\"  \u00a0\u00a0\u00a0\u00a0parser.add_argument(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"-c\", \"--config\", type=str, default=config_file, help=\"Config file\"  \u00a0\u00a0\u00a0\u00a0)  \u00a0\u00a0\u00a0\u00a0args = parser.parse_known_args()  \u00a0\u00a0\u00a0\u00a0if args and args[0].config:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0config_file = args[0].config  \u00a0\u00a0\u00a0\u00a0config = load_config(config_file)  \u00a0\u00a0\u00a0\u00a0return config<\/code><\/pre>\n<p>msgspec \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0438\u0437 \u0441\u043b\u043e\u0432\u0430\u0440\u0435\u0439 \u0442\u0430\u043a, \u043a\u0430\u043a \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 pydantic, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 YAML \u0432 \u043d\u0430\u0448\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b. \u042d\u0442\u043e \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0431\u043e\u043b\u0435\u0435 \u044f\u0432\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434, \u043d\u043e \u043e\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0444\u0438\u043b\u043e\u0441\u043e\u0444\u0438\u0438 msgspec: \u0431\u044b\u0442\u044c \u043b\u0451\u0433\u043a\u0438\u043c \u0438 \u0431\u044b\u0441\u0442\u0440\u044b\u043c.<\/p>\n<h4>\u0421\u043e\u0437\u0434\u0430\u0451\u043c settings-example.yaml<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u0444\u0430\u0439\u043b settings-example.yaml \u0441 \u0442\u0430\u043a\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043e\u0439, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u043b\u0430 \u043d\u0430\u0448\u0438\u043c \u043a\u043b\u0430\u0441\u0441\u0430\u043c:<\/p>\n<pre><code class=\"yaml\">database:  \u00a0\u00a0host: 127.0.0.1  \u00a0\u00a0port: 5432  \u00a0\u00a0user: postgres  \u00a0\u00a0password: postgres  \u00a0\u00a0database_name: ltcats_test_db  \u00a0\u00a0#url: \"postgresql+asyncpg:\/\/postgres:postgres@localhost\/ltcats_test_db\" local  \u00a0\u00a0url: \"postgresql+asyncpg:\/\/postgres:postgres@localhost\/ltcats_test_db\" # docker  \u00a0\u00a0echo: true  keydb:  \u00a0\u00a0host: 127.0.0.1  \u00a0\u00a0port: 6379  \u00a0\u00a0db: 0  \u00a0\u00a0url: \"keydb:\/\/localhost:6379\/0\"  jwt:  \u00a0\u00a0secret: \"your-secret-key\"  \u00a0\u00a0token_secret: \"super-secret\"<\/code><\/pre>\n<p>\ud83d\udccc \u041f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435: \u0432 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0435 \u043b\u0443\u0447\u0448\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0441\u0435\u043a\u0440\u0435\u0442\u044b \u0432 \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u043c secrets manager (Vault, AWS\/GCP\/Azure Secret Manager), \u043d\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u044b \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c YAML. \u0415\u0441\u043b\u0438 \u0437\u0430\u0445\u043e\u0442\u0438\u0442\u0435, \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 .env \u0447\u0435\u0440\u0435\u0437 os.getenv \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0443\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443.<\/p>\n<h4>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c app.py<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u043c src\/<a href=\"http:\/\/app.py\">app.py<\/a>, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0448\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0438:<\/p>\n<pre><code class=\"python\">from litestar import Litestar  from litestar.di import Provide  from src.controllers.user import UserController, provide_users_repo  from src.controllers.role import RoleController  from src.controllers.user_role import UserRoleController  from src.controllers.cv import CVController  from src.controllers.work_experience import WorkExperienceController  from src.controllers.company import CompanyController  from src.controllers.educational_institution import EducationalInstitutionController  from src.controllers.education import EducationController  from litestar.contrib.sqlalchemy.plugins import (  \u00a0\u00a0\u00a0\u00a0AsyncSessionConfig,  \u00a0\u00a0\u00a0\u00a0SQLAlchemyAsyncConfig,  \u00a0\u00a0\u00a0\u00a0SQLAlchemyInitPlugin,  )  from litestar.connection import ASGIConnection  from litestar.openapi import OpenAPIConfig  from litestar.params import Parameter  from litestar.plugins.sqlalchemy import filters, base  from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin  from litestar.security.jwt import JWTAuth, Token  from litestar.logging import LoggingConfig  from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine  from uuid import UUID  from src.configs.app_config import configure  from src.postgres.models.user import User  from jinja2 import Environment, PackageLoader, select_autoescape  # \u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0438  config = configure()  logging_config = LoggingConfig(  \u00a0\u00a0\u00a0\u00a0root={\"level\": \"INFO\", \"handlers\": [\"queue_listener\"]},  \u00a0\u00a0\u00a0\u00a0formatters={  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"standard\": {\"format\": \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"}  \u00a0\u00a0\u00a0\u00a0},  \u00a0\u00a0\u00a0\u00a0log_exceptions=\"always\",\u00a0 # \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0441 \u0442\u0440\u0430\u0441\u0441\u0438\u0440\u043e\u0432\u043a\u043e\u0439  )  env = Environment(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loader=PackageLoader(\"src\"),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0autoescape=select_autoescape()  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)  async def provide_limit_offset_pagination(  \u00a0\u00a0\u00a0\u00a0current_page: int = Parameter(ge=1, query=\"currentPage\", default=1, required=False),  \u00a0\u00a0\u00a0\u00a0page_size: int = Parameter(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0query=\"pageSize\",  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ge=1,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0default=10,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0required=False,  \u00a0\u00a0\u00a0\u00a0),  ) -&gt; filters.LimitOffset:  \u00a0\u00a0\u00a0\u00a0\"\"\"Add offset\/limit pagination.  \u00a0\u00a0\u00a0\u00a0Return type consumed by Repository.apply_limit_offset_pagination().  \u00a0\u00a0\u00a0\u00a0Parameters  \u00a0\u00a0\u00a0\u00a0----------  \u00a0\u00a0\u00a0\u00a0current_page : int  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0LIMIT to apply to select.  \u00a0\u00a0\u00a0\u00a0page_size : int  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0OFFSET to apply to select.  \u00a0\u00a0\u00a0\u00a0\"\"\"  \u00a0\u00a0\u00a0\u00a0return filters.LimitOffset(page_size, page_size * (current_page - 1))  sessionmaker = async_sessionmaker(expire_on_commit=False)  async def retrieve_user_handler(  \u00a0\u00a0\u00a0\u00a0token: Token,  \u00a0\u00a0\u00a0\u00a0connection: ASGIConnection,  ) -&gt; User | None:  \u00a0\u00a0\u00a0\u00a0user_id = UUID(token.sub)  \u00a0\u00a0\u00a0\u00a0users_repo = connection.scope.get(\"users_repo\")  \u00a0\u00a0\u00a0\u00a0if not users_repo:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0async with sessionmaker(bind=db_config.get_engine()) as session:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0try:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0async with session.begin():  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0users_repo = await provide_users_repo(db_session=session)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0except IntegrityError as exc:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0raise ClientException(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0status_code=HTTP_409_CONFLICT,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0detail=str(exc),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) from exc  \u00a0\u00a0\u00a0\u00a0user = await users_repo.get(user_id)  \u00a0\u00a0\u00a0\u00a0return user  jwt_auth = JWTAuth[User](  \u00a0\u00a0\u00a0\u00a0retrieve_user_handler=retrieve_user_handler,  \u00a0\u00a0\u00a0\u00a0token_secret=config.jwt.token_secret,  \u00a0\u00a0\u00a0\u00a0algorithm=\"HS256\",  \u00a0\u00a0\u00a0\u00a0exclude=[\"\/users\", \"\/schema\"]  )  session_config = AsyncSessionConfig(expire_on_commit=False)  db_config = SQLAlchemyAsyncConfig(  \u00a0\u00a0\u00a0\u00a0connection_string=config.database.get_connection_url(),  \u00a0\u00a0\u00a0\u00a0before_send_handler=\"autocommit\",  \u00a0\u00a0\u00a0\u00a0session_config=session_config,  )  async def on_startup() -&gt; None:  \u00a0\u00a0\u00a0\u00a0\"\"\"Initializes the database.\"\"\"  \u00a0\u00a0\u00a0\u00a0async with db_config.get_engine().begin() as conn:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await conn.run_sync(base.UUIDBase.metadata.create_all)  sqlalchemy_plugin = SQLAlchemyInitPlugin(config=db_config)  app = Litestar(  \u00a0\u00a0\u00a0\u00a0route_handlers=[  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UserController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0RoleController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UserRoleController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CVController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0WorkExperienceController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CompanyController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0EducationalInstitutionController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0EducationController,  \u00a0\u00a0\u00a0\u00a0],  \u00a0\u00a0\u00a0\u00a0on_startup=[on_startup],  \u00a0\u00a0\u00a0\u00a0on_app_init=[jwt_auth.on_app_init],  \u00a0\u00a0\u00a0\u00a0openapi_config=OpenAPIConfig(title=\"My API\", version=\"1.0.0\"),  \u00a0\u00a0\u00a0\u00a0dependencies={  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"limit_offset\": Provide(provide_limit_offset_pagination),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0},  \u00a0\u00a0\u00a0\u00a0plugins=[sqlalchemy_plugin, SQLAlchemySerializationPlugin()],  \u00a0\u00a0\u00a0\u00a0logging_config=logging_config,  )  <\/code><\/pre>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435:<\/p>\n<p><code>make run<\/code><\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0437\u043d\u0430\u0447\u0438\u0442, \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. \u0422\u0435\u043f\u0435\u0440\u044c \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0435\u0434\u0438\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a, \u0438 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c msgspec \u0434\u043b\u044f \u043a\u043e\u043d\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u0438 \u0441 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c \u2014 \u043a\u043e\u0442\u0438\u043a\u0438 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u044b, \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u043d\u0430\u0432\u0435\u0434\u0451\u043d! \ud83d\udc3e<\/p>\n<h3>\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 JWT \u2014 \u00ab\u0423\u0441\u044b, \u043b\u0430\u043f\u044b \u0438 \u0445\u0432\u043e\u0441\u0442 \u2014 \u0432\u043e\u0442 \u043c\u043e\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b!\u00bb \ud83d\udd11<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/920\/545\/4c0\/9205454c0bb4b3689ddaeb7a8e65aad9.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/920\/545\/4c0\/9205454c0bb4b3689ddaeb7a8e65aad9.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/920\/545\/4c0\/9205454c0bb4b3689ddaeb7a8e65aad9.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u2014 \u043f\u0435\u0440\u0432\u043e\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u043a\u043e\u0448\u0430\u0447\u044c\u0435\u0433\u043e \u043a\u043b\u0443\u0431\u0430. \u041c\u044b \u043d\u0435 \u0445\u043e\u0442\u0438\u043c, \u0447\u0442\u043e\u0431\u044b \u0411\u0430\u0440\u0441\u0438\u043a \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043b \u0440\u0435\u0437\u044e\u043c\u0435 \u041c\u0443\u0440\u0437\u0438\u043a\u0430 \u0431\u0435\u0437 \u0441\u043f\u0440\u043e\u0441\u0430! \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 JWT, \u043f\u0440\u0438\u0447\u0451\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0438\u0437 Litestar \u2014 \u0431\u044b\u0441\u0442\u0440\u043e, \u043d\u0430\u0434\u0451\u0436\u043d\u043e \u0438 \u0431\u0435\u0437 \u043b\u0438\u0448\u043d\u0438\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439. \u0422\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0442\u0438\u043a\u0438 \u0441 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u043e\u043c \u0441\u043c\u043e\u0433\u0443\u0442 \u0432\u043e\u0439\u0442\u0438! \ud83d\ude3a<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u043c JWT \u0447\u0435\u0440\u0435\u0437 JWTAuth \u0438\u0437 Litestar.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \/login \u0438 \u0437\u0430\u0449\u0438\u0442\u0438\u043c CRUD-\u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c \u043c\u043e\u0434\u0435\u043b\u044c User \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u043e\u043b\u044f.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043f\u0430\u0440\u043e\u043b\u044c \u0432 \u043c\u043e\u0434\u0435\u043b\u044c<\/h4>\n<p>\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u043f\u043e\u043b\u0435 \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0445\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u0430\u0440\u043e\u043b\u044f \u0432 \u043d\u0430\u0448\u0443 \u043c\u043e\u0434\u0435\u043b\u044c User. \u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c src\/postgres\/models\/<a href=\"http:\/\/user.py\">user.py<\/a> \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c:<\/p>\n<pre><code class=\"python\">from sqlalchemy import String  from sqlalchemy import CheckConstraint, Index  from sqlalchemy.orm import Mapped, mapped_column, relationship  from litestar.plugins.sqlalchemy import base  from typing import Optional  class User(base.UUIDAuditBase):  \u00a0\u00a0\u00a0\u00a0tablename = \"users\"  \u00a0\u00a0\u00a0\u00a0first_name: Mapped[str] = mapped_column(String(50), nullable=False)  \u00a0\u00a0\u00a0\u00a0last_name: Mapped[str] = mapped_column(String(50), nullable=True)  \u00a0\u00a0\u00a0\u00a0email: Mapped[str] = mapped_column(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0String(255), nullable=False, unique=True, index=True  \u00a0\u00a0\u00a0\u00a0)  \u00a0\u00a0\u00a0\u00a0hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)  \u00a0\u00a0\u00a0\u00a0profile_photo_url: Mapped[Optional[str]] = mapped_column(String(255))  \u00a0\u00a0\u00a0\u00a0user_roles: Mapped[list[\"UserRole\"]] = relationship(back_populates=\"user\")  \u00a0\u00a0\u00a0\u00a0cvs: Mapped[list[\"CV\"]] = relationship(back_populates=\"user\")  \u00a0\u00a0\u00a0\u00a0__table_args__ = (  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CheckConstraint(\"length(first_name) &gt; 0\", name=\"check_first_name_not_empty\"),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CheckConstraint(\"length(last_name) &gt; 0\", name=\"check_last_name_not_empty\"),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CheckConstraint(\"email LIKE '%@%.%'\", name=\"check_email_format\"),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Index(\"idx_users_created_at\", \"created_at\"),  \u00a0\u00a0\u00a0\u00a0)<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0443\u0436\u043d\u043e \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044e, \u0447\u0442\u043e\u0431\u044b \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0437\u043d\u0430\u043b\u0430 \u043e \u043d\u043e\u0432\u043e\u043c \u043f\u043e\u043b\u0435:<\/p>\n<pre><code class=\"bash\">make revision msg=\"add hashed_password to users\"  make upgrade<\/code><\/pre>\n<h4>\u042d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \u043b\u043e\u0433\u0438\u043d\u0430 \u0438 \u0437\u0430\u0449\u0438\u0442\u0430 CRUD<\/h4>\n<p>\u0414\u043b\u044f \u0445\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u043d\u0430\u043c \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f passlib, \u0442\u0430\u043a \u0447\u0442\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c \u0435\u0433\u043e:<\/p>\n<p>uv add &#171;passlib[bcrypt]&#187;<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c src\/controllers\/<a href=\"http:\/\/user.py\">user.py<\/a>:<\/p>\n<pre><code class=\"python\">from litestar.di import Provide  from src.models.users import UserLogin  )  from passlib.context import CryptContext  pwd_context = CryptContext(schemes=[\"sha256_crypt\"])  class UserController(Controller):  \u00a0\u00a0\u00a0\u00a0path = \"\/users\"  \u00a0\u00a0\u00a0\u00a0dependencies = {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"users_repo\": Provide(provide_users_repo),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0@post(\"\/login\", signature_types=[User])\u00a0 # \u041e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u043c \u0437\u0430\u0449\u0438\u0442\u0443 \u0434\u043b\u044f \u043b\u043e\u0433\u0438\u043d\u0430  \u00a0\u00a0\u00a0\u00a0async def login(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0self,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0data: UserLogin,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0users_repo: UsersRepository,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) -&gt; dict:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0user = await users_repo.get_one_or_none(email=data.email)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if not user or not pwd_context.verify(data.password, user.hashed_password):  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0raise HTTPException(status_code=401, detail=\"\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 email \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c\")  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0token = app.jwt_auth.create_token(identifier=str(user.id))  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return {\"access_token\": token, \"token_type\": \"bearer\"}<\/code><\/pre>\n<p>\u00a0\u00a0\u00a0<\/p>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u0414\u0430\u0432\u0430\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u0451\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f (\u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0443\u0431\u0440\u0430\u0442\u044c \u0437\u0430\u0449\u0438\u0442\u0443 \u0441 \/users):<\/p>\n<pre><code class=\"bash\">curl -X POST http:\/\/localhost:8000\/users -H \"Content-Type: application\/json\" -d '{\"first_name\": \"\u041c\u0443\u0440\u0437\u0438\u043a\", \"last_name\": \"\u041a\u043e\u0442\u043e\u0432\", \"email\": \"murzik@example.com\"}'<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043b\u043e\u0433\u0438\u043d\u0438\u043c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \/login:<\/p>\n<pre><code class=\"bash\">curl -X POST http:\/\/localhost:8000\/users\/login -H \"Content-Type: application\/json\" -d '{\"first_name\": \"\u041c\u0443\u0440\u0437\u0438\u043a\", \"last_name\": \"\u041a\u043e\u0442\u043e\u0432\", \"email\": \"murzik@example.com\"}'<\/code><\/pre>\n<p>\u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0442\u043e\u043a\u0435\u043d \u0432 \u043e\u0442\u0432\u0435\u0442\u0435, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:<\/p>\n<pre><code class=\"json\">{  \"access_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",  \"token_type\": \"bearer\"  }<\/code><\/pre>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0437\u0430\u0449\u0438\u0449\u0451\u043d\u043d\u044b\u043c \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430\u043c:<\/p>\n<pre><code class=\"bash\">curl -X GET http:\/\/localhost:8000\/users\/&lt;user_id&gt; -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"<\/code><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0442\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0442\u0438\u043a\u0438 \u0441 \u043a\u043e\u0448\u0430\u0447\u044c\u0438\u043c \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u043e\u043c \u043c\u043e\u0433\u0443\u0442 \u0432\u043e\u0439\u0442\u0438! JWT \u043e\u0442 Litestar \u2014 \u044d\u0442\u043e \u043a\u0430\u043a \u043c\u0438\u0441\u043a\u0430 \u0441 \u043a\u043e\u0440\u043c\u043e\u043c: \u043f\u0440\u043e\u0441\u0442\u043e, \u0432\u043a\u0443\u0441\u043d\u043e \u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e. \ud83d\udc3e<\/p>\n<h3>\u041a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 KeyDB \u2014 \u043a\u043e\u0448\u0430\u0447\u044c\u044f \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u26a1<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/396\/e55\/3d0\/396e553d03c1bbfced016d9bcb4cb56d.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/396\/e55\/3d0\/396e553d03c1bbfced016d9bcb4cb56d.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/396\/e55\/3d0\/396e553d03c1bbfced016d9bcb4cb56d.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u0415\u0441\u043b\u0438 \u043a\u043e\u0442\u0438\u043a\u0438 \u043d\u0430\u0447\u043d\u0443\u0442 \u043c\u0430\u0441\u0441\u043e\u0432\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c \u0440\u0435\u0437\u044e\u043c\u0435, \u043d\u0430\u0448\u0430 \u0431\u0430\u0437\u0430 \u0441\u043a\u0430\u0436\u0435\u0442: \u00ab\u041c\u044f\u0443, \u044f \u0443\u0441\u0442\u0430\u043b\u0430!\u00bb KeyDB \u0441 \u0435\u0433\u043e \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u0442\u043e\u0447\u043d\u044b\u043c\u0438 \u043b\u0430\u043f\u043a\u0430\u043c\u0438 \u0443\u0441\u043a\u043e\u0440\u0438\u0442 \u0432\u0441\u0451, \u0447\u0442\u043e\u0431\u044b \u043a\u043e\u0442\u0438\u043a\u0438 \u043b\u0435\u0442\u0430\u043b\u0438, \u0430 \u0431\u0430\u0437\u0430 \u043e\u0442\u0434\u044b\u0445\u0430\u043b\u0430. \u041c\u044b \u043e\u0431\u0435\u0449\u0430\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c KeyDB \u0435\u0449\u0451 \u0432 \u043f\u0435\u0440\u0432\u043e\u0439 \u0447\u0430\u0441\u0442\u0438, \u0442\u0430\u043a \u0447\u0442\u043e \u043f\u043e\u0440\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043e\u0431\u0435\u0449\u0430\u043d\u0438\u044f! \ud83d\ude3a<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c KeyDB \u0447\u0435\u0440\u0435\u0437 Docker.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430 \/users\/{user_id}.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>\u0417\u0430\u043f\u0443\u0441\u043a KeyDB<\/h4>\n<p>\u041f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u043c KeyDB \u0432 Docker:<\/p>\n<p><code>docker run -d -p 6379:6379 eqalpha\/keydb<\/code><\/p>\n<h4>\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c<\/h4>\n<p>\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 KeyDB:<\/p>\n<p><code>uv add \"redis[hiredis]\"<\/code><\/p>\n<h3>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430<\/h3>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u0444\u0430\u0439\u043b src\/clients\/<a href=\"http:\/\/cache.py\">cache.py<\/a> \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 KeyDB, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043d\u0430\u0448 \u043a\u043e\u043d\u0444\u0438\u0433:<\/p>\n<pre><code class=\"python\">import redis.asyncio as redis  from src.configs.app_config import configure  config = configure()  keydb = redis.Redis(host=config.keydb.host, port=config.keydb.port, db=config.keydb.db)<\/code><\/pre>\n<h4>\u041a\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0435<\/h4>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c \u043c\u0435\u0442\u043e\u0434 get_user \u0432 src\/controllers\/<a href=\"http:\/\/user.py\">user.py<\/a>, \u0447\u0442\u043e\u0431\u044b \u043e\u043d \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u043b \u043a\u0435\u0448, \u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0442\u043e\u043c \u043b\u0435\u0437 \u0432 \u0431\u0430\u0437\u0443. \u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0442\u0430\u043a\u0436\u0435 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \u0434\u043b\u044f \u0440\u0435\u0437\u044e\u043c\u0435 (\u043e \u043d\u0451\u043c \u043f\u043e\u0437\u0436\u0435):<\/p>\n<pre><code class=\"python\">from src.clients.cache import keydb  from litestar.response import Template  logger = logging.getLogger(__name__)  pwd_context = CryptContext(schemes=[\"sha256_crypt\"])  class UsersRepository(repository.SQLAlchemyAsyncRepository[User]):  \u00a0\u00a0\u00a0\u00a0\"\"\"Author repository.\"\"\"  \u00a0\u00a0\u00a0\u00a0model_type = User  async def provide_users_repo(db_session: AsyncSession) -&gt; UsersRepository:  \u00a0\u00a0\u00a0\u00a0\"\"\"This provides the default Authors repository.\"\"\"  \u00a0\u00a0\u00a0\u00a0return UsersRepository(session=db_session)  class UserController(Controller):  \u00a0\u00a0\u00a0\u00a0path = \"\/users\"  \u00a0\u00a0\u00a0\u00a0dependencies = {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"users_repo\": Provide(provide_users_repo),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0@get(\"\/{user_id:uuid}\", return_dto=MsgspecDTO[UserRead])  \u00a0\u00a0\u00a0\u00a0async def get_user(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0self,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0user_id: UUID,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0users_repo: UsersRepository,  \u00a0\u00a0\u00a0\u00a0) -&gt; UserRead:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"\"\"Get an existing author.\"\"\"  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0cache_key = f\"user:{user_id}\"  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0cached = await keydb.get(cache_key)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if cached:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return json.decode(cached, type=UserRead)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0user = await users_repo.get(user_id)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await keydb.set(cache_key, json.encode(user.to_dict()), ex=3600)\u00a0 # \u041a\u044d\u0448 \u043d\u0430 \u0447\u0430\u0441  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return user  . . .  \u00a0\u00a0\u00a0\u00a0@get(\"\/{user_id:uuid}\/cv\", media_type=\"text\/html\")  \u00a0\u00a0\u00a0\u00a0async def get_user_resume(self, user_id: UUID, users_repo: UsersRepository) -&gt; Template:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0user = await users_repo.get(user_id)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0template = app.env.get_template(\"cv.html\")  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return Template(template=template, context={\"user\": user})<\/code><\/pre>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u0417\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0447\u0435\u0440\u0435\u0437 curl:<\/p>\n<p><code>curl -X GET <\/code><a href=\"http:\/\/localhost:8000\/users\/&lt;user_id&gt;\"><code>http:\/\/localhost:8000\/users\/&lt;user_id&gt;<\/code><\/a><code> -H \"Authorization: Bearer &lt;your-token&gt;\"<\/code><\/p>\n<p>\u041f\u0435\u0440\u0432\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043f\u043e\u0439\u0434\u0451\u0442 \u0432 \u0431\u0430\u0437\u0443, \u0430 \u0432\u0442\u043e\u0440\u043e\u0439 \u2014 \u0443\u0436\u0435 \u0438\u0437 \u043a\u0435\u0448\u0430, \u0438 \u0431\u0443\u0434\u0435\u0442 \u0431\u044b\u0441\u0442\u0440\u0435\u0435. KeyDB \u2014 \u044d\u0442\u043e \u043a\u0430\u043a \u043a\u043e\u0448\u0430\u0447\u044c\u044f \u043c\u044f\u0442\u0430 \u0434\u043b\u044f API: \u043a\u043e\u0442\u0438\u043a\u0438 \u043b\u0435\u0442\u0430\u044e\u0442, \u0430 \u0431\u0430\u0437\u0430 \u043e\u0442\u0434\u044b\u0445\u0430\u0435\u0442. \u041c\u044f\u0443-\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430! \u26a1<\/p>\n<h3>\u041f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0441 Coverage \u2014 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u043a\u043e\u0448\u0430\u0447\u044c\u044e \u043d\u0430\u0434\u0451\u0436\u043d\u043e\u0441\u0442\u044c \ud83e\uddea<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/813\/4a9\/89b\/8134a989bd9e6523f3db40d31bf3be27.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/813\/4a9\/89b\/8134a989bd9e6523f3db40d31bf3be27.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/813\/4a9\/89b\/8134a989bd9e6523f3db40d31bf3be27.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u0423 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0442\u0435\u0441\u0442\u044b, \u043d\u043e \u043a\u0430\u043a \u043f\u043e\u043d\u044f\u0442\u044c, \u0432\u0441\u0451 \u043b\u0438 \u043c\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043b\u0438? Coverage \u043f\u043e\u043a\u0430\u0436\u0435\u0442, \u0433\u0434\u0435 \u043a\u043e\u0442\u0438\u043a\u0438 \u0435\u0449\u0451 \u043d\u0435 \u043f\u0440\u043e\u0448\u043b\u0438\u0441\u044c \u043b\u0430\u043f\u043a\u0430\u043c\u0438, \u0438 \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043d\u0430\u0448 \u043a\u043e\u0434 \u043d\u0430\u0434\u0451\u0436\u0435\u043d \u043a\u0430\u043a \u043a\u043e\u0448\u0430\u0447\u044c\u044f \u0438\u043d\u0442\u0443\u0438\u0446\u0438\u044f. \ud83d\ude38<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c coverage.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u043c \u0437\u0430\u043f\u0443\u0441\u043a \u0442\u0435\u0441\u0442\u043e\u0432 \u0441 \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435\u043c \u0438 \u0432\u044b\u0432\u0435\u0434\u0435\u043c \u043e\u0442\u0447\u0451\u0442.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430<\/h4>\n<p>\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c coverage:<\/p>\n<p><code>uv add coverage<\/code><\/p>\n<h4>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430<\/h4>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c Makefile, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u044b \u0441 \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435\u043c:<\/p>\n<pre><code class=\"yaml\">test-coverage:  uv run coverage run -m pytest  test-coverage-report:  uv run coverage report --show-missing<\/code><\/pre>\n<h4>\u0417\u0430\u043f\u0443\u0441\u043a<\/h4>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c:<\/p>\n<p><code>make test-coverage-report<\/code><\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u0432\u044b\u0432\u043e\u0434\u0430:<\/p>\n<pre><code class=\"bash\">Name                    Stmts   Miss  Cover   Missing ----------------------------------------------------- src\/app.py                 20      2    90%   15-16 src\/controllers\/users.py   50     10    80%   25-30, 45-50 src\/models\/users.py        10      0   100% ----------------------------------------------------- TOTAL                      80     12    85%<\/code><\/pre>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u041e\u0442\u0447\u0451\u0442 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442, \u0447\u0442\u043e \u0443 \u043d\u0430\u0441 85% \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u044f \u2014 \u043d\u0435\u043f\u043b\u043e\u0445\u043e, \u043d\u043e \u0435\u0441\u0442\u044c \u043a\u0443\u0434\u0430 \u0440\u0430\u0441\u0442\u0438! \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0441\u0442\u0440\u043e\u043a\u0438 25-30 \u0432 <a href=\"http:\/\/users.py\">users.py<\/a> \u2014 \u044d\u0442\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043e\u0448\u0438\u0431\u043e\u043a \u0432 \/login, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u044b \u043d\u0435 \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0438. \u0414\u0430\u0432\u0430\u0439 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0442\u0435\u0441\u0442 \u0432 src\/tests\/test_<a href=\"http:\/\/users.py\">users.py<\/a>:<\/p>\n<pre><code class=\"python\">@pytest.mark.asyncio  async def test_login_invalid_credentials(db_session: AsyncSession):  user_data = UserCreate(first_name=\"\u0412\u0430\u0441\u044c\u043a\u0430\", last_name=\"\u041c\u0443\u0440\u0437\u0438\u043a\u043e\u0432\", email=\"vasya@whiskers.com\")  user = User(**structs.asdict(user_data), hashed_password=pwd_context.hash(\"wrong-pass\"))  db_session.add(user)  await db_session.commit()  with pytest.raises(HTTPException) as exc:  await UserController().login(user_data, UsersRepository(session=db_session))  assert exc.value.status_code == 401<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u043c make test-coverage \u2014 \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u0434\u0440\u0430\u0441\u0442\u0438! Coverage \u2014 \u044d\u0442\u043e \u043a\u0430\u043a \u0432\u0435\u0442\u0435\u0440\u0438\u043d\u0430\u0440 \u0434\u043b\u044f \u043a\u043e\u0434\u0430: \u0441\u0440\u0430\u0437\u0443 \u0432\u0438\u0434\u043d\u043e, \u0433\u0434\u0435 \u043a\u043e\u0442\u0438\u043a \u043f\u0440\u0438\u0445\u0440\u0430\u043c\u044b\u0432\u0430\u0435\u0442. \u041f\u043e\u0440\u0430 \u043f\u043e\u0434\u0442\u044f\u043d\u0443\u0442\u044c \u0445\u0432\u043e\u0441\u0442\u044b! \ud83d\udc3e<\/p>\n<h3>\u0428\u0430\u0431\u043b\u043e\u043d \u0440\u0435\u0437\u044e\u043c\u0435 \u0441 Jinja \u2014 \u043a\u043e\u0448\u0430\u0447\u044c\u0438 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0438<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fad\/d68\/e2b\/fadd68e2b8cbba845401a5772d845fbf.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/fad\/d68\/e2b\/fadd68e2b8cbba845401a5772d845fbf.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fad\/d68\/e2b\/fadd68e2b8cbba845401a5772d845fbf.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>API \u2014 \u044d\u0442\u043e \u043a\u0440\u0443\u0442\u043e, \u043d\u043e \u0445\u043e\u0447\u0435\u0442\u0441\u044f \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0440\u0435\u0437\u044e\u043c\u0435 \u043a\u043e\u0442\u0438\u043a\u043e\u0432 \u0432\u0436\u0438\u0432\u0443\u044e! \u0421\u0434\u0435\u043b\u0430\u0435\u043c \u0448\u0430\u0431\u043b\u043e\u043d \u0432 \u0441\u0442\u0438\u043b\u0435 \u0431\u0430\u0441\u043a\u0435\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043a\u0430\u0440\u0442\u043e\u0447\u0435\u043a 90-\u0445: \u0437\u0432\u0451\u0437\u0434\u044b \u0441\u0432\u0435\u0440\u0445\u0443, \u0438\u043c\u044f-\u0444\u0430\u043c\u0438\u043b\u0438\u044f, \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435, \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0430 \u0441 \u043e\u043f\u044b\u0442\u043e\u043c \u0440\u0430\u0431\u043e\u0442\u044b. \u041c\u0438\u043d\u0438\u043c\u0443\u043c JS \u0438 CSS, \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0438\u0441\u0442\u044b\u0439 \u043a\u043e\u0448\u0430\u0447\u0438\u0439 \u0448\u0438\u043a! \ud83d\ude3a<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043c jinja2.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u043c\u043e\u0434\u0435\u043b\u044c \u0438 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u044e\u043c\u0435 (\u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438 \u0432 <a href=\"http:\/\/users.py\">users.py<\/a>).<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c HTML-\u0448\u0430\u0431\u043b\u043e\u043d \u0432 \u0441\u0442\u0438\u043b\u0435 \u0431\u0430\u0441\u043a\u0435\u0442\u0431\u043e\u043b\u044c\u043d\u044b\u0445 \u043a\u0430\u0440\u0442\u043e\u0447\u0435\u043a.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 Jinja<\/h4>\n<p>\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c jinja2:<\/p>\n<p><code>uv add jinja2<\/code><\/p>\n<h4>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Jinja<\/h4>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0432 \u0444\u0430\u0439\u043b src\/<a href=\"http:\/\/app.py\">app.py<\/a> \u043a\u043e\u0434 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0448\u0430\u0431\u043b\u043e\u043d\u0430\u043c\u0438:<\/p>\n<pre><code class=\"python\">from jinja2 import Environment, PackageLoader, select_autoescape  env = Environment(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loader=PackageLoader(\"src\"),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0autoescape=select_autoescape()  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)<\/code><\/pre>\n<h4>\u0428\u0430\u0431\u043b\u043e\u043d cv.html<\/h4>\n<p>\u0428\u0440\u0438\u0444\u0442 \u043d\u0430 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0435 \u043f\u043e\u0445\u043e\u0436 \u043d\u0430 Impact \u0434\u043b\u044f \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u043e\u0432 \u0438 Arial \u0434\u043b\u044f \u0442\u0435\u043a\u0441\u0442\u0430. \u041c\u044b \u0443\u0431\u0440\u0430\u043b\u0438 \u0444\u043e\u0442\u043e, \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438 \u0437\u0432\u0451\u0437\u0434\u044b, \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0438 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0443 \u0441 \u043e\u043f\u044b\u0442\u043e\u043c \u0440\u0430\u0431\u043e\u0442\u044b. \u0421\u043e\u0437\u0434\u0430\u0451\u043c templates\/cv.html:<\/p>\n<pre><code class=\"xml\">&lt;!DOCTYPE html&gt;  &lt;html&gt;  &lt;head&gt;  \u00a0\u00a0\u00a0\u00a0&lt;title&gt;{{ user.first_name }} {{ user.last_name }} - Resume&lt;\/title&gt;  \u00a0\u00a0\u00a0\u00a0&lt;style&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.card {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0border: 2px solid #000;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0width: 400px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0margin: 20px auto;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0background: #f0f0f0;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-family: Arial, sans-serif;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.3);  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.stars {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-align: center;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-size: 24px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0color: #ff4500;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0padding: 5px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.header {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0background: #ff4500;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0color: white;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-align: center;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0padding: 10px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-family: Impact, sans-serif;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-size: 24px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-transform: uppercase;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.subheader {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-family: Impact, sans-serif;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-size: 14px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0color: #ff4500;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-align: center;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0margin: 5px 0;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.info {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0padding: 10px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-size: 14px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.description {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-style: italic;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0margin: 10px 0;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.stats {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0margin: 10px 0;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.stats div {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0margin: 5px 0;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.experience {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0border-top: 2px solid #ff4500;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0padding: 10px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.experience h3 {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-family: Impact, sans-serif;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-size: 16px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-align: center;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0margin-bottom: 10px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0color: #000;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.experience table {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0width: 100%;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0border-collapse: collapse;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-size: 12px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.experience th, .experience td {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0border: 1px solid #000;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0padding: 5px;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text-align: left;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0.experience th {  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0background: #ff4500;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0color: white;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0font-family: Impact, sans-serif;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}  \u00a0\u00a0\u00a0\u00a0&lt;\/style&gt;  &lt;\/head&gt;  &lt;body&gt;  \u00a0\u00a0\u00a0\u00a0&lt;div class=\"card\"&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"stars\"&gt;\u2b50\u2b50\u2b50\u2b50\u2b50&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"header\"&gt;{{ user.first_name }} {{ user.last_name }}&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"subheader\"&gt;PROFESSIONAL CAT CODER&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"info\"&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"stats\"&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div&gt;&lt;b&gt;Email:&lt;\/b&gt; {{ user.email }}&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div&gt;&lt;b&gt;Joined:&lt;\/b&gt; {{ user.created_at.strftime('%Y-%m-%d') }}&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"description\"&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{{ user.first_name }} is a purr-fect coder with a knack for catching bugs faster than a laser pointer. Known for napping on keyboards and delivering meow-nificent code, this cat is a true asset to any team!  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div class=\"experience\"&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;h3&gt;WORK EXPERIENCE&lt;\/h3&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;table&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;tr&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;th&gt;Company&lt;\/th&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;th&gt;Position&lt;\/th&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;th&gt;Tech Stack&lt;\/th&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/tr&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{% for cv in user.cvs %}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{%- for exp in cv.work_experiences %}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;tr&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;td&gt;{{ exp.company }}&lt;\/td&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;td&gt;{{ exp.job_title }}&lt;\/td&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;td&gt;{{ exp.employment_type }}&lt;\/td&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;td&gt;{{ exp.start_date }}&lt;\/td&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;td&gt;{{ exp.end_date }}&lt;\/td&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;td&gt;{{ exp.description }}&lt;\/td&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/tr&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{% endfor %}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{% endfor %}  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/table&gt;  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;  \u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;  &lt;\/body&gt;  &lt;\/html&gt;  <\/code><\/pre>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0447\u0435\u0440\u0435\u0437 SQL \u0438\u043b\u0438 API, \u0447\u0442\u043e\u0431\u044b \u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0431\u044b\u043b \u043e\u043f\u044b\u0442 \u0440\u0430\u0431\u043e\u0442\u044b. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:<\/p>\n<pre><code class=\"sql\">INSERT INTO work_experience (user_id, company, position, description, created_at, updated_at)  VALUES ('&lt;user_id&gt;', 'CatTech', 'Senior Whisker Engineer', 'Python, MeowSQL', NOW(), NOW());<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 <a href=\"http:\/\/localhost:8000\/users\/&lt;user_id&gt;\/cv\">http:\/\/localhost:8000\/users\/&lt;user_id&gt;\/cv<\/a>. \u0412\u044b \u0443\u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0442\u0438\u043b\u044c\u043d\u0443\u044e \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0443: \u0437\u0432\u0451\u0437\u0434\u044b \u0441\u0432\u0435\u0440\u0445\u0443, \u0438\u043c\u044f-\u0444\u0430\u043c\u0438\u043b\u0438\u044f, \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0438 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0430 \u0441 \u043e\u043f\u044b\u0442\u043e\u043c \u0440\u0430\u0431\u043e\u0442\u044b. \u0411\u0435\u0437 \u043b\u0438\u0448\u043d\u0435\u0433\u043e JS \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0438\u0441\u0442\u044b\u0439 \u043a\u043e\u0448\u0430\u0447\u0438\u0439 \u0448\u0438\u043a!<\/p>\n<h3>\u0423\u043f\u0430\u043a\u043e\u0432\u043a\u0430 \u0432 Docker \u2014 \u043a\u043e\u0442\u0438\u043a\u0438 \u0432 \u043a\u043e\u0440\u043e\u0431\u043a\u0435 \ud83d\udce6<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/017\/30c\/405\/01730c405ff438b1a989a59a9165de5c.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/017\/30c\/405\/01730c405ff438b1a989a59a9165de5c.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/017\/30c\/405\/01730c405ff438b1a989a59a9165de5c.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u0412 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0435 \u043a\u043e\u0434 \u0431\u0435\u0437 Docker \u2014 \u043a\u0430\u043a \u043a\u043e\u0442\u0438\u043a \u0431\u0435\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438. \u0423\u043f\u0430\u043a\u0443\u0435\u043c \u0432\u0441\u0451 \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0448 API \u0431\u044b\u043b \u0433\u043e\u0442\u043e\u0432 \u043a \u0434\u0435\u043f\u043b\u043e\u044e! \ud83d\udce6<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c Dockerfile.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c docker-compose.yaml.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>Dockerfile<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c Dockerfile:<\/p>\n<pre><code class=\"bash\">FROM python:3.13.3-slim  WORKDIR \/app  # \u041a\u043e\u043f\u0438\u0440\u0443\u0435\u043c \u0444\u0430\u0439\u043b\u044b \u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044f\u043c\u0438 \u0438 \u043a\u043e\u043d\u0444\u0438\u0433  COPY pyproject.toml uv.lock .\/  COPY src\/configs\/settings-example.yaml .\/configs\/  # \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c uv (\u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0432 \u0431\u0430\u0437\u043e\u0432\u043e\u043c \u043e\u0431\u0440\u0430\u0437\u0435)  RUN pip install --no-cache-dir uv  # \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 uv  RUN uv pip install --system -r pyproject.toml  # \u041a\u043e\u043f\u0438\u0440\u0443\u0435\u043c \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0444\u0430\u0439\u043b\u044b  COPY src\/ .\/src\/  COPY src\/templates\/ .\/templates\/  COPY src\/configs\/alembic.ini .\/configs\/  # \u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 granian  CMD [\"granian\", \"--interface\", \"asgi\", \"src.app:app\"]<\/code><\/pre>\n<h4>\u041e\u0431\u043d\u043e\u0432\u043b\u0451\u043d\u043d\u044b\u0439 docker-compose.yaml<\/h4>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c docker-compose.yaml, \u0447\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c KeyDB \u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435:<\/p>\n<pre><code class=\"yaml\">services:  \u00a0\u00a0app:  \u00a0\u00a0\u00a0\u00a0build: .  \u00a0\u00a0\u00a0\u00a0ports:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- \"8000:8000\"  \u00a0\u00a0\u00a0\u00a0depends_on:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0postgres:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0condition: service_healthy  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0keydb:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0condition: service_healthy  \u00a0\u00a0\u00a0\u00a0networks:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- litestarcats  \u00a0\u00a0postgres:  \u00a0\u00a0\u00a0\u00a0image: postgres:latest  \u00a0\u00a0\u00a0\u00a0container_name: postgres  \u00a0\u00a0\u00a0\u00a0hostname: postgres  \u00a0\u00a0\u00a0\u00a0ports:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- 5432:5432  \u00a0\u00a0\u00a0\u00a0volumes:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- \"postgres-data:\/var\/lib\/postgresql\/data\"  \u00a0\u00a0\u00a0\u00a0networks:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- litestarcats  \u00a0\u00a0\u00a0\u00a0healthcheck:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0interval: 10s  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0timeout: 5s  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0retries: 3  \u00a0\u00a0keydb:  \u00a0\u00a0\u00a0\u00a0image: eqalpha\/keydb:latest  \u00a0\u00a0\u00a0\u00a0container_name: keydb  \u00a0\u00a0\u00a0\u00a0ports:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- \"6379:6379\"  \u00a0\u00a0\u00a0\u00a0volumes:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- keydb_data:\/data  \u00a0\u00a0\u00a0\u00a0restart: unless-stopped  \u00a0\u00a0\u00a0\u00a0networks:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- litestarcats  \u00a0\u00a0\u00a0\u00a0healthcheck:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0test: [\"CMD-SHELL\", \"redis-cli ping\"]  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0interval: 10s  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0timeout: 5s  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0retries: 3  volumes:  \u00a0\u00a0postgres-data:  \u00a0\u00a0keydb_data:  \u00a0\u00a0\u00a0\u00a0driver: local  networks:  \u00a0\u00a0litestarcats:  \u00a0\u00a0\u00a0\u00a0driver: bridge  <\/code><\/pre>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c:<\/p>\n<p><code>docker compose up --build<\/code><\/p>\n<p>\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c, \u0447\u0442\u043e API \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043d\u0430 <a href=\"http:\/\/localhost:8000\">http:\/\/localhost:8000<\/a> \u0438 \u0440\u0435\u0437\u044e\u043c\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f. \u041a\u043e\u0442\u0438\u043a\u0438 \u0432 \u043a\u043e\u0440\u043e\u0431\u043a\u0435, \u0430 \u043f\u0440\u043e\u0435\u043a\u0442 \u0432 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0435! Docker \u2014 \u044d\u0442\u043e \u043a\u0430\u043a \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u043a\u0430 \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e API. \ud83d\udc3e<\/p>\n<h3>\u041a\u043e\u0442\u0438\u043a\u0438 \u0433\u043e\u0442\u043e\u0432\u044b \u043a \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0443! \ud83c\udf89<\/h3>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cb1\/550\/9bd\/cb15509bd484fd1f94cc2c730f506b99.jpg\" width=\"1280\" height=\"720\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/cb1\/550\/9bd\/cb15509bd484fd1f94cc2c730f506b99.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cb1\/550\/9bd\/cb15509bd484fd1f94cc2c730f506b99.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<h4>\u0418\u0442\u043e\u0433\u0438<\/h4>\n<p>\u041c\u044b \u0432\u044b\u043d\u0435\u0441\u043b\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e msgspec, \u0434\u043e\u0431\u0430\u0432\u0438\u043b\u0438 JWT \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438, KeyDB \u0434\u043b\u044f \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438, coverage \u0434\u043b\u044f \u043d\u0430\u0434\u0451\u0436\u043d\u043e\u0441\u0442\u0438, Jinja \u0434\u043b\u044f \u0441\u0442\u0438\u043b\u044f \u0438 Docker \u0434\u043b\u044f \u0434\u0435\u043f\u043b\u043e\u044f. \u041d\u0430\u0448 \u043a\u043e\u0448\u0430\u0447\u0438\u0439 API \u0442\u0435\u043f\u0435\u0440\u044c \u0433\u043e\u0442\u043e\u0432 \u043a \u0431\u043e\u044e! \ud83d\ude3a<\/p>\n<h4>\u0412\u043f\u0435\u0447\u0430\u0442\u043b\u0435\u043d\u0438\u044f<\/h4>\n<p>Litestar \u2014 \u044d\u0442\u043e \u043d\u0430\u0445\u043e\u0434\u043a\u0430, KeyDB \u043b\u0435\u0442\u0430\u0435\u0442, \u0430 msgspec \u0441\u0434\u0435\u043b\u0430\u043b \u043a\u043e\u0434 \u0435\u0449\u0451 \u0431\u044b\u0441\u0442\u0440\u0435\u0435 \u0438 \u043a\u043e\u043d\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043d\u0435\u0435. \u041a\u0430\u0440\u0442\u043e\u0447\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438\u0441\u044c \u043d\u0430 \u0443\u0440\u0430, \u041c\u0443\u0440\u0437\u0438\u043a \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043a\u0430\u043a \u0437\u0432\u0435\u0437\u0434\u0430! \u2b50<\/p>\n<h4>\u0427\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435?<\/h4>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0447\u0430\u0441\u0442\u0438 \u043c\u043e\u0436\u043d\u043e Litestar \u0441 FastAPI. \u0410 \u043c\u043e\u0436\u0435\u0442, \u043a\u043e\u0448\u0430\u0447\u044c\u0438 \u0430\u0432\u0430\u0442\u0430\u0440\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 API? \u041f\u0438\u0448\u0438\u0442\u0435 \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u044f\u0445, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0432 \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438! \ud83d\udc3e<\/p>\n<h4>\u0421\u0441\u044b\u043b\u043a\u0430<\/h4>\n<p>\u041a\u043e\u0434 \u043d\u0430<a href=\"https:\/\/github.com\/pulichkin\/litestarcats\"> <u>GitHub<\/u><\/a>. \u041b\u0430\u043f\u043a\u0438 \u0432\u0432\u0435\u0440\u0445, \u0435\u0441\u043b\u0438 \u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u043e\u0441\u044c! \ud83d\ude3b<\/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\/901852\/\"> https:\/\/habr.com\/ru\/articles\/901852\/<\/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<h3>\u041a\u043e\u0442\u0438\u043a\u0438 \u0432\u044b\u0445\u043e\u0434\u044f\u0442 \u043d\u0430 \u043d\u043e\u0432\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c! \ud83d\udc3e<\/h3>\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u043a\u043e\u0442\u0438\u043a\u0438 \u0438 \u043a\u043e\u0442\u043e\u043b\u044e\u0431\u044b! \u0412 <a href=\"https:\/\/habr.com\/ru\/companies\/ntechlab\/articles\/883578\/\">\u043f\u0435\u0440\u0432\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/a> \u043d\u0430\u0448\u0435\u0433\u043e \u043a\u043e\u0448\u0430\u0447\u044c\u0435\u0433\u043e \u043f\u0440\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b (Litestar \u0432\u043c\u0435\u0441\u0442\u043e FastAPI, Granian \u0432\u043c\u0435\u0441\u0442\u043e Gunicorn, KeyDB \u0432\u043c\u0435\u0441\u0442\u043e Redis), \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043b\u0438 uv \u0438 \u0437\u0430\u043b\u043e\u0436\u0438\u043b\u0438 \u0444\u0443\u043d\u0434\u0430\u043c\u0435\u043d\u0442 \u043f\u0440\u043e\u0435\u043a\u0442\u0430. <strong>\u0412\u043e <\/strong><a href=\"https:\/\/habr.com\/ru\/companies\/ntechlab\/articles\/889022\/\"><code><strong>\u0432\u0442\u043e\u0440\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/strong><\/code><\/a><strong> <\/strong>\u043c\u044b \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u043b\u0438 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u043e\u0435 CRUD API \u0434\u043b\u044f \u0440\u0435\u0437\u044e\u043c\u0435 \u043a\u043e\u0442\u0438\u043a\u043e\u0432 (\u0438\u043b\u0438 \u043b\u044e\u0434\u0435\u0439, \u0435\u0441\u043b\u0438 \u0432\u0430\u043c \u0442\u0430\u043a \u0431\u043b\u0438\u0436\u0435), \u043f\u043e\u0434\u0440\u0443\u0436\u0438\u043b\u0438 \u0435\u0433\u043e \u0441 PostgreSQL \u0447\u0435\u0440\u0435\u0437 SQLAlchemy, \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u043b\u0438 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Alembic \u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0442\u0435\u0441\u0442\u044b \u0441 Pytest. \u0423 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u0441\u0442\u0435\u043d\u044b \u0438 \u0444\u0443\u043d\u0434\u0430\u043c\u0435\u043d\u0442, \u043d\u043e \u043f\u043e\u0440\u0430 \u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043a\u0440\u044b\u0448\u0443 \u0438 \u0433\u043e\u0442\u043e\u0432\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0443! \ud83c\udfe0<\/p>\n<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u043c\u044b \u0441\u0434\u0435\u043b\u0430\u0435\u043c \u043d\u0430\u0448 API \u0435\u0449\u0451 \u043a\u0440\u0443\u0447\u0435: \u0432\u044b\u043d\u0435\u0441\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e msgspec, \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 JWT \u0432 Litestar, \u0443\u0441\u043a\u043e\u0440\u0438\u043c API \u0441 KeyDB, \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u043c \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0441 coverage, \u0443\u043f\u0430\u043a\u0443\u0435\u043c \u0432\u0441\u0451 \u0432 Docker \u0438 \u043d\u0430\u0440\u0438\u0441\u0443\u0435\u043c \u0440\u0435\u0437\u044e\u043c\u0435 \u043a\u043e\u0442\u0438\u043a\u043e\u0432 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Jinja. \u041a \u043a\u043e\u043d\u0446\u0443 \u0441\u0442\u0430\u0442\u044c\u0438 \u043d\u0430\u0448 \u043a\u043e\u0448\u0430\u0447\u0438\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432 \u043a \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0436\u0438\u0437\u043d\u0438 \u2014 \u043f\u043e\u0435\u0445\u0430\u043b\u0438! \ud83d\ude80<\/p>\n<figure class=\"full-width\"><\/figure>\n<hr\/>\n<p>\u0415\u0441\u043b\u0438 \u0440\u0430\u043d\u044c\u0448\u0435 \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u043e \u0442\u0440\u0435\u043d\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u043d\u0430 \u043a\u043e\u0448\u043a\u0430\u0445, \u0442\u043e \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u044b\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0438\u0445 \u0432 \u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u043c\u0438\u0440 \u0441 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043c\u0438, \u043a\u0435\u0448\u0435\u043c \u0438 \u0441\u0442\u0438\u043b\u044c\u043d\u044b\u043c\u0438 \u0440\u0435\u0437\u044e\u043c\u0435. \u041f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u0434, \u043a\u0430\u043a \u0432\u0441\u0435\u0433\u0434\u0430, \u043d\u0430 GitHub \u2014 \u0441\u0441\u044b\u043b\u043a\u0430 \u0432 \u043a\u043e\u043d\u0446\u0435! \ud83d\udc31<\/p>\n<h3>\u0412\u044b\u043d\u043e\u0441 \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432 \u2014 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u043c \u0432\u0441\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432 \u043f\u043e\u0440\u044f\u0434\u043e\u043a \ud83d\uddc2\ufe0f<\/h3>\n<figure class=\"full-width\"><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u041d\u0430\u0448 \u043f\u0440\u043e\u0435\u043a\u0442 \u0440\u0430\u0441\u0442\u0451\u0442, \u0438 \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u0440\u043e\u0434\u0435 DATABASE_URL \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0442 \u043c\u044f\u0443\u043a\u0430\u0442\u044c \u043e\u0442 \u043d\u0435\u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430. \u0414\u0430\u0432\u0430\u0439 \u0432\u044b\u043d\u0435\u0441\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c src\/configs\/app_<a href=\"http:\/\/config.py\">config.py<\/a> \u0438 \u0431\u0443\u0434\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 settings-example.yaml. \u041c\u044b \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c msgspec \u0434\u043b\u044f \u043c\u043e\u0434\u0435\u043b\u0435\u0439, \u0442\u0430\u043a \u0447\u0442\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u043c \u0435\u0433\u043e \u0438 \u0434\u043b\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432 \u2014 \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0435\u0442 \u043a\u043e\u0434 \u0447\u0438\u0449\u0435 \u0438 \u0431\u044b\u0441\u0442\u0440\u0435\u0435, \u0430 \u043a\u043e\u0442\u0438\u043a\u0438 \u043b\u044e\u0431\u044f\u0442 \u043f\u043e\u0440\u044f\u0434\u043e\u043a! \ud83d\ude3a<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e msgspec.Struct.<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u043c \u043f\u0430\u0440\u0441\u0438\u043d\u0433 settings-example.yaml \u0432 \u044d\u0442\u0438 \u043a\u043b\u0430\u0441\u0441\u044b.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c src\/<a href=\"http:\/\/app.py\">app.py<\/a>, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0438.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0414\u0435\u0442\u0430\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438<\/h4>\n<h4>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439<\/h4>\n<p>\u041d\u0430\u043c \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f pyyaml \u0434\u043b\u044f \u043f\u0430\u0440\u0441\u0438\u043d\u0433\u0430 YAML. msgspec \u0443 \u043d\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435:<\/p>\n<p><code>uv add pyyaml<\/code><\/p>\n<h4>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u043a\u043b\u0430\u0441\u0441\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u0444\u0430\u0439\u043b src\/configs\/app_<a href=\"http:\/\/config.py\">config.py<\/a>. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c msgspec.Struct \u0434\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u043e\u0432:<\/p>\n<pre><code class=\"python\">from msgspec import Struct  import argparse  import yaml  from pathlib import Path  class DatabaseConfig(Struct):  \u00a0\u00a0\u00a0\u00a0user: str = \"postgres\"  \u00a0\u00a0\u00a0\u00a0password: str = \"postgres\"  \u00a0\u00a0\u00a0\u00a0host: str = \"127.0.0.1\"  \u00a0\u00a0\u00a0\u00a0port: str = \"5432\"  \u00a0\u00a0\u00a0\u00a0database_name: str = \"test_db\"  \u00a0\u00a0\u00a0\u00a0url: str = None  \u00a0\u00a0\u00a0\u00a0echo: bool = False  \u00a0\u00a0\u00a0\u00a0def get_connection_url(self) -&gt; str:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if self.url:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return self.url  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return f\"postgresql+asyncpg:\/\/{self.user}:{self.password}@{self.host}:{self.port}\/{self.database_name}\"  class KeyDBConfig(Struct):  \u00a0\u00a0\u00a0\u00a0host: str = \"127.0.0.1\"  \u00a0\u00a0\u00a0\u00a0port: str = \"6379\"  \u00a0\u00a0\u00a0\u00a0db: str = \"0\"  \u00a0\u00a0\u00a0\u00a0url: str = None  \u00a0\u00a0\u00a0\u00a0def get_connection_url(self) -&gt; str:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if self.url:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return self.url  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0return f\"keydb:\/\/{self.host}:{self.port}\/{self.db}\"  class JWTConfig(Struct):  \u00a0\u00a0\u00a0\u00a0secret: str  \u00a0\u00a0\u00a0\u00a0token_secret: str  class AppConfig(Struct):  \u00a0\u00a0\u00a0\u00a0database: DatabaseConfig  \u00a0\u00a0\u00a0\u00a0keydb: KeyDBConfig  \u00a0\u00a0\u00a0\u00a0jwt: JWTConfig  def load_config(file_path: str) -&gt; AppConfig:  \u00a0\u00a0\u00a0\u00a0with open(file_path, \"r\") as f:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0config_data = yaml.safe_load(f)  \u00a0\u00a0\u00a0\u00a0return AppConfig(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0database=DatabaseConfig(**config_data[\"database\"]),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0keydb=KeyDBConfig(**config_data[\"keydb\"]),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0jwt=JWTConfig(**config_data[\"jwt\"]),  \u00a0\u00a0\u00a0\u00a0)  def configure() -&gt; AppConfig:  \u00a0\u00a0\u00a0\u00a0title = \"LitestarCats\"  \u00a0\u00a0\u00a0\u00a0parser = argparse.ArgumentParser(title)  \u00a0\u00a0\u00a0\u00a0src_dir = Path(__file__).absolute().parent  \u00a0\u00a0\u00a0\u00a0config_file = src_dir \/ \"settings-example.yaml\"  \u00a0\u00a0\u00a0\u00a0parser.add_argument(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"-c\", \"--config\", type=str, default=config_file, help=\"Config file\"  \u00a0\u00a0\u00a0\u00a0)  \u00a0\u00a0\u00a0\u00a0args = parser.parse_known_args()  \u00a0\u00a0\u00a0\u00a0if args and args[0].config:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0config_file = args[0].config  \u00a0\u00a0\u00a0\u00a0config = load_config(config_file)  \u00a0\u00a0\u00a0\u00a0return config<\/code><\/pre>\n<p>msgspec \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0438\u0437 \u0441\u043b\u043e\u0432\u0430\u0440\u0435\u0439 \u0442\u0430\u043a, \u043a\u0430\u043a \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 pydantic, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 YAML \u0432 \u043d\u0430\u0448\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b. \u042d\u0442\u043e \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0431\u043e\u043b\u0435\u0435 \u044f\u0432\u043d\u044b\u0439 \u043f\u043e\u0434\u0445\u043e\u0434, \u043d\u043e \u043e\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0444\u0438\u043b\u043e\u0441\u043e\u0444\u0438\u0438 msgspec: \u0431\u044b\u0442\u044c \u043b\u0451\u0433\u043a\u0438\u043c \u0438 \u0431\u044b\u0441\u0442\u0440\u044b\u043c.<\/p>\n<h4>\u0421\u043e\u0437\u0434\u0430\u0451\u043c settings-example.yaml<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u043c \u0444\u0430\u0439\u043b settings-example.yaml \u0441 \u0442\u0430\u043a\u043e\u0439 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043e\u0439, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u043b\u0430 \u043d\u0430\u0448\u0438\u043c \u043a\u043b\u0430\u0441\u0441\u0430\u043c:<\/p>\n<pre><code class=\"yaml\">database:  \u00a0\u00a0host: 127.0.0.1  \u00a0\u00a0port: 5432  \u00a0\u00a0user: postgres  \u00a0\u00a0password: postgres  \u00a0\u00a0database_name: ltcats_test_db  \u00a0\u00a0#url: \"postgresql+asyncpg:\/\/postgres:postgres@localhost\/ltcats_test_db\" local  \u00a0\u00a0url: \"postgresql+asyncpg:\/\/postgres:postgres@localhost\/ltcats_test_db\" # docker  \u00a0\u00a0echo: true  keydb:  \u00a0\u00a0host: 127.0.0.1  \u00a0\u00a0port: 6379  \u00a0\u00a0db: 0  \u00a0\u00a0url: \"keydb:\/\/localhost:6379\/0\"  jwt:  \u00a0\u00a0secret: \"your-secret-key\"  \u00a0\u00a0token_secret: \"super-secret\"<\/code><\/pre>\n<p>\ud83d\udccc \u041f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435: \u0432 \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u0435\u043d\u0435 \u043b\u0443\u0447\u0448\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0441\u0435\u043a\u0440\u0435\u0442\u044b \u0432 \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u043c secrets manager (Vault, AWS\/GCP\/Azure Secret Manager), \u043d\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u044b \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c YAML. \u0415\u0441\u043b\u0438 \u0437\u0430\u0445\u043e\u0442\u0438\u0442\u0435, \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 .env \u0447\u0435\u0440\u0435\u0437 os.getenv \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u0443\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443.<\/p>\n<h4>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c app.py<\/h4>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u043c src\/<a href=\"http:\/\/app.py\">app.py<\/a>, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0448\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0438:<\/p>\n<pre><code class=\"python\">from litestar import Litestar  from litestar.di import Provide  from src.controllers.user import UserController, provide_users_repo  from src.controllers.role import RoleController  from src.controllers.user_role import UserRoleController  from src.controllers.cv import CVController  from src.controllers.work_experience import WorkExperienceController  from src.controllers.company import CompanyController  from src.controllers.educational_institution import EducationalInstitutionController  from src.controllers.education import EducationController  from litestar.contrib.sqlalchemy.plugins import (  \u00a0\u00a0\u00a0\u00a0AsyncSessionConfig,  \u00a0\u00a0\u00a0\u00a0SQLAlchemyAsyncConfig,  \u00a0\u00a0\u00a0\u00a0SQLAlchemyInitPlugin,  )  from litestar.connection import ASGIConnection  from litestar.openapi import OpenAPIConfig  from litestar.params import Parameter  from litestar.plugins.sqlalchemy import filters, base  from litestar.plugins.sqlalchemy import SQLAlchemySerializationPlugin  from litestar.security.jwt import JWTAuth, Token  from litestar.logging import LoggingConfig  from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine  from uuid import UUID  from src.configs.app_config import configure  from src.postgres.models.user import User  from jinja2 import Environment, PackageLoader, select_autoescape  # \u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0438  config = configure()  logging_config = LoggingConfig(  \u00a0\u00a0\u00a0\u00a0root={\"level\": \"INFO\", \"handlers\": [\"queue_listener\"]},  \u00a0\u00a0\u00a0\u00a0formatters={  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"standard\": {\"format\": \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"}  \u00a0\u00a0\u00a0\u00a0},  \u00a0\u00a0\u00a0\u00a0log_exceptions=\"always\",\u00a0 # \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0441 \u0442\u0440\u0430\u0441\u0441\u0438\u0440\u043e\u0432\u043a\u043e\u0439  )  env = Environment(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0loader=PackageLoader(\"src\"),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0autoescape=select_autoescape()  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)  async def provide_limit_offset_pagination(  \u00a0\u00a0\u00a0\u00a0current_page: int = Parameter(ge=1, query=\"currentPage\", default=1, required=False),  \u00a0\u00a0\u00a0\u00a0page_size: int = Parameter(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0query=\"pageSize\",  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0ge=1,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0default=10,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0required=False,  \u00a0\u00a0\u00a0\u00a0),  ) -&gt; filters.LimitOffset:  \u00a0\u00a0\u00a0\u00a0\"\"\"Add offset\/limit pagination.  \u00a0\u00a0\u00a0\u00a0Return type consumed by Repository.apply_limit_offset_pagination().  \u00a0\u00a0\u00a0\u00a0Parameters  \u00a0\u00a0\u00a0\u00a0----------  \u00a0\u00a0\u00a0\u00a0current_page : int  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0LIMIT to apply to select.  \u00a0\u00a0\u00a0\u00a0page_size : int  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0OFFSET to apply to select.  \u00a0\u00a0\u00a0\u00a0\"\"\"  \u00a0\u00a0\u00a0\u00a0return filters.LimitOffset(page_size, page_size * (current_page - 1))  sessionmaker = async_sessionmaker(expire_on_commit=False)  async def retrieve_user_handler(  \u00a0\u00a0\u00a0\u00a0token: Token,  \u00a0\u00a0\u00a0\u00a0connection: ASGIConnection,  ) -&gt; User | None:  \u00a0\u00a0\u00a0\u00a0user_id = UUID(token.sub)  \u00a0\u00a0\u00a0\u00a0users_repo = connection.scope.get(\"users_repo\")  \u00a0\u00a0\u00a0\u00a0if not users_repo:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0async with sessionmaker(bind=db_config.get_engine()) as session:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0try:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0async with session.begin():  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0users_repo = await provide_users_repo(db_session=session)  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0except IntegrityError as exc:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0raise ClientException(  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0status_code=HTTP_409_CONFLICT,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0detail=str(exc),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0) from exc  \u00a0\u00a0\u00a0\u00a0user = await users_repo.get(user_id)  \u00a0\u00a0\u00a0\u00a0return user  jwt_auth = JWTAuth[User](  \u00a0\u00a0\u00a0\u00a0retrieve_user_handler=retrieve_user_handler,  \u00a0\u00a0\u00a0\u00a0token_secret=config.jwt.token_secret,  \u00a0\u00a0\u00a0\u00a0algorithm=\"HS256\",  \u00a0\u00a0\u00a0\u00a0exclude=[\"\/users\", \"\/schema\"]  )  session_config = AsyncSessionConfig(expire_on_commit=False)  db_config = SQLAlchemyAsyncConfig(  \u00a0\u00a0\u00a0\u00a0connection_string=config.database.get_connection_url(),  \u00a0\u00a0\u00a0\u00a0before_send_handler=\"autocommit\",  \u00a0\u00a0\u00a0\u00a0session_config=session_config,  )  async def on_startup() -&gt; None:  \u00a0\u00a0\u00a0\u00a0\"\"\"Initializes the database.\"\"\"  \u00a0\u00a0\u00a0\u00a0async with db_config.get_engine().begin() as conn:  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0await conn.run_sync(base.UUIDBase.metadata.create_all)  sqlalchemy_plugin = SQLAlchemyInitPlugin(config=db_config)  app = Litestar(  \u00a0\u00a0\u00a0\u00a0route_handlers=[  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UserController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0RoleController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0UserRoleController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CVController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0WorkExperienceController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0CompanyController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0EducationalInstitutionController,  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0EducationController,  \u00a0\u00a0\u00a0\u00a0],  \u00a0\u00a0\u00a0\u00a0on_startup=[on_startup],  \u00a0\u00a0\u00a0\u00a0on_app_init=[jwt_auth.on_app_init],  \u00a0\u00a0\u00a0\u00a0openapi_config=OpenAPIConfig(title=\"My API\", version=\"1.0.0\"),  \u00a0\u00a0\u00a0\u00a0dependencies={  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\"limit_offset\": Provide(provide_limit_offset_pagination),  \u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0},  \u00a0\u00a0\u00a0\u00a0plugins=[sqlalchemy_plugin, SQLAlchemySerializationPlugin()],  \u00a0\u00a0\u00a0\u00a0logging_config=logging_config,  )  <\/code><\/pre>\n<h4>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430<\/h4>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435:<\/p>\n<p><code>make run<\/code><\/p>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0441\u0451 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0437\u043d\u0430\u0447\u0438\u0442, \u043a\u043e\u043d\u0444\u0438\u0433\u0438 \u043f\u043e\u0434\u0433\u0440\u0443\u0436\u0430\u044e\u0442\u0441\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. \u0422\u0435\u043f\u0435\u0440\u044c \u0443 \u043d\u0430\u0441 \u0435\u0441\u0442\u044c \u0435\u0434\u0438\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a, \u0438 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c msgspec \u0434\u043b\u044f \u043a\u043e\u043d\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u0438 \u0441 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c \u2014 \u043a\u043e\u0442\u0438\u043a\u0438 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u044b, \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u043d\u0430\u0432\u0435\u0434\u0451\u043d! \ud83d\udc3e<\/p>\n<h3>\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 JWT \u2014 \u00ab\u0423\u0441\u044b, \u043b\u0430\u043f\u044b \u0438 \u0445\u0432\u043e\u0441\u0442 \u2014 \u0432\u043e\u0442 \u043c\u043e\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b!\u00bb \ud83d\udd11<\/h3>\n<figure class=\"full-width\"><\/figure>\n<h4>\u0417\u0430\u0447\u0435\u043c \u044d\u0442\u043e \u043d\u0443\u0436\u043d\u043e?<\/h4>\n<p>\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u2014 \u043f\u0435\u0440\u0432\u043e\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u043a\u043e\u0448\u0430\u0447\u044c\u0435\u0433\u043e \u043a\u043b\u0443\u0431\u0430. \u041c\u044b \u043d\u0435 \u0445\u043e\u0442\u0438\u043c, \u0447\u0442\u043e\u0431\u044b \u0411\u0430\u0440\u0441\u0438\u043a \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043b \u0440\u0435\u0437\u044e\u043c\u0435 \u041c\u0443\u0440\u0437\u0438\u043a\u0430 \u0431\u0435\u0437 \u0441\u043f\u0440\u043e\u0441\u0430! \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 JWT, \u043f\u0440\u0438\u0447\u0451\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u043c\u043e\u0434\u0443\u043b\u044c \u0438\u0437 Litestar \u2014 \u0431\u044b\u0441\u0442\u0440\u043e, \u043d\u0430\u0434\u0451\u0436\u043d\u043e \u0438 \u0431\u0435\u0437 \u043b\u0438\u0448\u043d\u0438\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439. \u0422\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0442\u0438\u043a\u0438 \u0441 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u043e\u043c \u0441\u043c\u043e\u0433\u0443\u0442 \u0432\u043e\u0439\u0442\u0438! \ud83d\ude3a<\/p>\n<h4>\u0427\u0442\u043e \u0431\u0443\u0434\u0435\u043c \u0434\u0435\u043b\u0430\u0442\u044c?<\/h4>\n<ul>\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u043c JWT \u0447\u0435\u0440\u0435\u0437 JWTAuth \u0438\u0437 Litestar.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u043c \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \/login \u0438 \u0437\u0430\u0449\u0438\u0442\u0438\u043c CRUD-\u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u0438\u043c \u043c\u043e\u0434\u0435\u043b\u044c<\/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-456435","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/456435","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=456435"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/456435\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=456435"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=456435"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=456435"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}