[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n# Inspired by Django .editorconfig file\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of_line = lf\ncharset = utf-8\n\n[*.md]\nmax_line_length = 80\n\n# Makefiles always use tabs for indentation\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n"
  },
  {
    "path": ".mdlrc.rb",
    "content": "all\nrule 'MD013', :line_length => 120\nrule 'MD024', :allow_different_nesting => true\nrule 'MD007', :indent => 4"
  },
  {
    "path": ".travis.yml",
    "content": "language: python\npython:\n  - \"3.9\"\ninstall:\n  - gem install chef-utils -v 16.6.14\n  - gem install mdl\n  - pip install -r requirements.txt\nscript:\n  # speller.yandex.net doesn't work anymore\n  # - rozental --reorder_vocabulary .\n  - mdl --style=.mdlrc.rb .\n"
  },
  {
    "path": ".vocabulary",
    "content": "# наш словарь\nдатаинженерный\nконфигу\nкрон-джобу\nмерджим\nотгрумленных\nперфоманс-ревью\nпул\nпул-реквест\nпул-реквестами\nпул-реквестах\nпул-реквестов\nпул-реквесты\nревью\nсингнатуре\nстейте\nтикету\nфидбек\nфикстура\nфичафлагах\nфичафлагов\nфичафлагом\nэфстринги\nюрлицам\n\n# нормальные слова, на которые ругается Я.Спеллер\nвовремя\nзелёный\nкарго-культа\nкузницах\nнаходиться\nопечаток\nпостфикс\nпрефиксами\nпроцессору\nсемантичным\nсократит\nувеличит\nулучшит\nформатировании\nфраншизе\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 BestDoctor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# `bestdoctor.guides`\n\n[![Build Status](https://travis-ci.org/best-doctor/guides.svg?branch=master)](https://travis-ci.org/best-doctor/guides)\n\nСборник гайдов, которым следует команда разработки BestDoctor.\n\nБудем рады, если они пригодятся и вам.\n\n## Руководства\n\n- [Python styleguide](./guides/python_styleguide.md) ([eng](./guides/en/python_styleguide.md))\n\n- [Frontend Styleguide LUCHI](./guides/frontend-styleguide-luchi.md)\n\n- [Guide-skill workflow](./guides/guide_skill_workflow.md)\n\n- [Python testing styleguide](./guides/python_test_styleguide.md)\n\n- [Team guide](./guides/team_guide.md)\n\n- [Performance review guide](./guides/performance_review_guide.md)\n\n- [API guide](./guides/api_guide.md)\n\n- [Opensource project guide](./guides/opensource_guide.md)\n\n- [Airflow guide](./guides/airflow_guide.md)\n\n- [Infrastructure security guide](./guides/infra_infosec_guide.md)\n"
  },
  {
    "path": "guides/airflow_guide.md",
    "content": "# Датаинженерный гайд по работе с Airflow\n\nВ этом гайде собраны договорённости, который мы соблюдаем при работе\nс Apache Airflow.\n\n## Структура кода\n\n### SQL\n\nВесь SQL лежит в папочке `sql`, раскиданный по отдельным файликам.\nОдин запрос – один файл.\n\n### DAG\n\nДаги сгруппированы по директориям, одна директория – один даг.\n\nСтруктура внутри директории такая:\n\n- `dag.py` - здесь только DAG, его параметры и операторы;\n- `helpers.py`;\n- `constants.py`;\n- `tests`:\n    - `test_dag.py`;\n    - `test_helpers.py`;\n    - `factories.py`.\n\n### Общий код\n\nФункции и операторы, которые используются в нескольких дагах, выносятся\nв папку `utils` и импортируются оттуда.\n\nСтруктура `utils`:\n\n- `operators`;\n- `sensors`;\n- `hooks`;\n- `utils`.\n\nВ названии файлов внутри директорий не дублируем тип сущности.\nТак плохо: `sensors/github_sensor.py`, так хорошо: `sensors/github.py`.\n\n## Модели\n\nНекоторые данные мы храним в PostgreSQL и используем SQLAlchemy\nдля работы с ними.\n\n- Каждая таблица находится в отдельном файле в папке models.\n  Нейминг файлов по названию таблиц (например `dataset_losses.py`).\n-Названия полей – `<наименование таблицы-источника>_<наименование поля>`,\n  например `patient_last_name`, `clinic_legal_title`.\n- В названиях моделей датасетов используем префикс `Dataset`.\n- В названиях моделей витрин используем префикс `Datawallmart`.\n\n## Миграции\n\n- Название новой миграции состоит из номера – номера миграций идут подряд,\n  независимо от изменений.\n- В названии после номера указываем суть изменений.\n\nПримеры:\n\n- `1_some_table_create.py`;\n- `2_some_other_table_create.py`;\n- `3_some_table_add_beauty_field.py`;\n- `4_dataset_my_precious_change_field_types.py`.\n\n## Pandas\n\nМы не используем Pandas. Верим, что стандартной библиотеки достаточно\nдля написания хорошего, понятного, поддерживаемого и быстрого кода.\n\nВозможно, с разрастанием кодовой базы мы переиграем это правило, но пока так.\n\n## Промежуточные результаты\n\nПромежуточные результаты храним в csv файлах в `/tmp` директории.\n\nСами файлы именуются по правилу `<dag_id>_<task_id>.csv` и имена\nхранятся в строковых литералах, у которых также есть префиксы:\n\n- `extract_` — для файлов, в которые экспортируются\n  первичные данные;\n- `temp_` — для промежуточных файлов после преобразования,\n  если из них данные не записываются сразу в таблицы;\n- `load` — для файлов с данными, которые загружаются\n  в базу данных.\n\n## Правила написания дагов\n\n- Мы используем контекст менеджеры и не передаем DAG внутрь оператора явно –\n  чтобы было абсолютно четко понятно из каких операторов состоит DAG.\n- `start_date` задается в виде статического `datetime` с обязательным\n  указанием часов и минут – потому что у Airflow есть проблемы\n  с динамическими датами при переходе через полночь, а также\n  чтобы всегда было явно понятно время с которого DAG начинает исполнятся.\n- `start_date` должен быть в прошлом всегда и к текущему моменту как\n  минимум один интервал должен умещаться в этом временном отрезке –\n  это позволяет избежать ненужных проблем со scheduler.\n- `schedule_interval` задается строкой с использованием cron-синтаксиса –\n  он короче, чем `timedelta`, более гибкий и больше нам нравится.\n- Мы не используем пресеты `@hourly`, `@daily` и т.д. – потому что\n  `explicit is better than implicit` – в кроне мы явно задаем\n  время исполнения DAG и контролируем это;\n- SubDag'и запрещены – под капотом у них SequentialExecutor, который\n  не годится для бое использования.\n- Для определения зависимостей между тасками используем оператор побитового\n  сдвига `>>` – потому что это наглядно и читается лучше чем\n  `some_task.set_upstream(some_other_task)`. При этом зависимости\n  задаются слева направо – потому что мы так читаем текст.\n\n## Обработка данных\n\n- General rule of thumb – если можно выполнить простую операцию на\n  стороне datasource, мы выполняем ее на стороне datasource.\n- Все результирующие датасеты (`dataset_` & `datawalmart_`) должны\n  содержать поле `last_modified_at` типа `timestamp`.\n- Все даты и времена хранятся в UTC.\n"
  },
  {
    "path": "guides/api_guide.md",
    "content": "# REST API BestDoctor\n\n## Общие принципы построения API\n\n1. API следует принципу HATEOAS, генерируя необходимые ссылки на ресурсы. Клиент опирается на доступные в ответе\n   ресурсы, а не на хардок ссылок API.\n1. API возвращает использует `HTTP` статусы в ответах. Успешная обработка запроса - `20x`, редирект - `30x`, ошибка в\n   запросе или при его обработке - 4xx.\n1. API информативен. Для успешной обработки запроса помимо ответа API может вернуть параметры запроса.  В случае ошибки\n   возвращается описание и массив ошибок валидатора, может быть ссылка на документацию по API и рекомендации как ошибки\n   избежать.\n1. Работать с API будут не столько машины, сколько разработчики, и мы хотим максимально им в этом помочь.\n1. API использует стандартные HTTP-заголовки. Версионирование, запрос конкретных feature, желаемый типа данных, передача\n   некоторых параметров типа TimeZone - все идет в заголовках и не замусоривает тело запроса повторяющимися параметрами.\n1. API быстрый. Ответ должен генерироваться максимум за 250мс, а тяжелые операции должны проводиться асинхронно.\n1. API это не (только лишь) CRUD над ORM, сущности API должны отражать бизнес-процессы. В дополнение к этому может\n   существовать и низкоуровневый слой API для манипуляции с базой.\n\n### Формат запроса и ответа\n\nВсе запросы и ответы передаются в формате json. Ответ содержит поле `data` в случае успешной обработки (код `200 OK`,\n`201 Created`), может не содержать тела (код `204 No Content`).\n\n```http request\nGET /patients/me HTTP/1.1\nAccept: application/vnd.bestdoctor.v1.verbose+json\n\nHTTP/1.1 200 OK\n{\n  \"data\": {\n    \"url\": \"/patients/51413\",\n    \"patient_id\": 51413,\n    \"name\": \"John\",\n    \"surname\": \"Doe\"\n  },\n  \"meta\": null,\n  \"query\": {\"patient_id\": \"me\"}\n}\n```\n\nКроме поля data ответ может содержать следующие поля:\n\n* `meta` - дополнительная информация по запросу, например, размер выборки для коллекций\n* `query` - разобранный запрос, включая дефолтные значения необязательных полей, не заданных в запросе. Отражает полные\n  входные данные обработчика запроса.\n\nКаждый объект содержит ссылку на себя в атрибуте `url` и ссылки на связанные ресурсы в атрибутах, имеющих суффикс `_url`.\n\nВ случае ошибки ответ содержит поле `message` с человекочитаемым описанием проблемы, массив `errors` может содержать\nподробности от валидаторов.\n\n```http request\nPOST /patients/me HTTP/1.1\n\nname=JOHN\n\nHTTP/1.1 401 Authorization Required\n{\"message\": \"You should be logged in\"}\n```\n\nКоды ответа `200`, `201`, `204` возвращаются для успешно обработанных запросов. При ошибках обработки 400, ошибках\nприложения `420`, ошибках валидации `422`, ошибках доступа `401`, `403`, `404` (когда необходимо).\n\n### Передача параметров\n\nПараметры для GET-запросов передаются в url и в query string после `?`. Остальные запросы принимают параметры в строке\nадреса и в теле запроса.\n\n```http request\nGET /patients?page=1 HTTP/1.1\n```\n\n```http request\nPOST /patients/51413 HTTP/1.1\n\n{\"name\": \"Vasiliy\"}\n```\n\n### Ошибки обработки запроса\n\nВ зависимости от схемы ответ с сообщением об ошибке может содержать поля:\n\n* `message` - человекочитаемое сообщение об ошибке (для разработчика или пользователя клиентского приложения)\n* `int_code` - числовой код ошибки для автоматической обработки клиентскими приложениями\n* `suppress_message` - флаг: можно ли не показывать сообщение пользователю клиентского приложения\n* `errors` - массив ошибок с указанием:\n    * `resource` - ресурса\n    * `field` - поля запроса\n    * `code` - буквенно-цифрового кода ошибки\n    * `message` - сообщения пользователю\n\n### Ошибка `400 Bad Request`\n\nПри ошибке разбора запроса (передан невалидный json) возвращается `400 Bad Request`\n\n```http request\nPATCH /patients/me HTTP/1.1\nHost: api.bestdoctor.ru\nAuthorization: Token hexdigest-token-string\n\nname=JOHN\n\nHTTP/1.1 400 Bad Request\n{\"message\": \"Body should be a valid json\"}\n```\n\n### Ошибка `420 Going To Hell Error`\n\nВсе ошибки, возникающие при обработке валидного запроса, возвращаются с кодом `420 Going To Hell Error`. Что-то пошло не\nтак с точки зрения сервера, но это не фатальная ошибка сервера типа `500`, а ошибка, предусмотренная бизнес-логикой.\n\n```http request\nPOST /patients/me/visits HTTP/1.1\nAuthorization: Token hexdigest-token-string\n\n{\"clinic_id\": 54321, \"timeslot\": \"2020-04-15T10:45:00+03:00\"}\n\nHTTP/1.1 420 Going To Hell Error\n{\n  \"message\": \"Выбранное время для записи в клинику недоступно\",\n  \"int_code\": 90210,\n  \"suppress_message\": false,\n  \"errors\": [\n    {\n      \"resource\": \"Clinic\",\n      \"code\": \"timeslot_is_busy\"\n    }\n  ]\n}\n```\n\n### Ошибка валидации запроса `422 Unprocessable Entity`\n\nПри ошибке валидации запроса - `422 Unprocessable Entity` + массив `errors` с описанием ошибок валидации.\n\n```http request\nPATCH /patients/me HTTP/1.1\nHost: api.bestdoctor.ru\nAuthorization: Token hexdigest-token-string\n\n{\"patient_id\": 12345, \"phone\": \"сотовый\"}\n\nHTTP/1.1 422 Unprocessable Entity\n{\n  \"message\": \"Validation error\",\n  \"errors\": [\n    {\n      \"resource\": \"Patient\",\n      \"field\": \"patient_id\",\n      \"code\": \"readonly_field\",\n      \"message\": \"Id пациента нельзя редактировать\"\n    },\n    {\n      \"resource\": \"Patient\",\n      \"field\": \"phone\",\n      \"code\": \"wrong_phone_format\",\n      \"message\": \"Телефон должен содержать хотя бы одну цифру\"\n    }\n  ]\n}\n```\n\n### Аутентификация клиента\n\nДанные для аутентификации клиента передаются в заголовке вида `Authorization: (basic|bearer|token) <value>`\n\n1. Basic-схема может использоваться в обычном режиме (login:password) для авторизации некоторых приватных методов API,\n   как базово поддерживаемая в url схема\n1. Basic-схема может использоваться в режиме login:OAuth для работы с OAuth и разделением прав разных приложений\n   (сайт, мобилка, премиальная мобилка)\n1. Bearer-схема может использоваться с сессионным токеном JWT\n1. Token-схема может использоваться для авторизации приложений по OAuth\n\nОтсутствие заголовка аутентификации может повлечь одно из следующего:\n\n* Успешный ответ, но явную или неявную фильтрацию/ограниченный объем ответа (к примеру, список публично доступных\n  ресурсов может быть меньше, чем для пациента или HR).\n* Ошибку `401 Authorization Required`\n\nПри успешной аутентификации клиента, но нехватке прав на конкретный запрашиваемый ресурс, API обычно отвечает `403 Not\nAuthorized`. В некоторых случаях стоит возвращать `404 Not Found,` чтобы скрыть сам факт наличия конкретного ресурса\n(клиенту, не имеющему должных прав, обращения к `/clients/nonexistent` и `/client/existing` должны оба вернуть `404`).\n\n### Паджинация (страничный вывод)\n\nМаксимальный размер страницы данных, которую возвращает API за один запрос, ограничен сервером, и может указываться в\nпараметре запроса `per_page` (дефолт 20, максимум 200). В ответе в разделе `meta` содержатся ссылки `url`, `next_url`,\n`prev_url` и `last_url` на текущую, следующую, предыдущую и последнюю страницы набора данных. Поле `page_tmpl_url`\nсодержит шаблон для формирования url страницы по её номеру.\n\nЧтобы скачать полный набор данных, клиенту следует запрашивать следующую страницу по адресу из `meta.next_url`, пока это\nполе присутствует в ответе\n\nВ разделе `meta` может присутствовать поле `total`, обозначающие общее количество элементов в наборе, и поле `pages`,\nобозначающее общее количество страниц (размера `per_page`) в наборе.\n\n#### Постраничная паджинация\n\nСтраница выбирается параметром `page` (1, 2, 3, ...).\n\n```http request\nGET /patients?per_page=10 HTTP/1.1\nHost: api.bestdoctor.ru\nAccept: application/vnd.bestdoctor.v1.verbose+json\n\nHTTP/1.1 200 OK\n{\n  \"data\": [{\"url\": \"patient_id\": 51413, \"name\": \"John\", \"surname\": \"Doe\"}, ...],\n  \"query\": {\"page\": 1, \"per_page\": 10},\n  \"meta\": {\n    \"total\": 98, \"pages\": 10,\n    \"page_tmpl_url\": \"/patients?page={page}&per_page=10\",\n    \"url\": \"/patients?page=1&per_page=10\",\n    \"next_url\": \"/patients?page=2&per_page=10\",\n    \"last_url\": \"/patients?page=10&per_page=10\"\n  }\n}\n```\n\n#### Относительная паджинация\n\nНекоторые ресурсы, например отсортированные по времени создания по убыванию, неудобно запрашивать по номеру страницы.\nТак `page=1` может всегда указывать на самые последние записи, однако два запроса одной и той же страницы могут вернуть\nразный набор данных, если он успел измениться.\n\nПараметр `before=[object_uuid]` или `after=[object_uuid]` задаёт две вещи: граничный элемент и направление сортировки\nрезультата.  Так, вариант с `before` запрашивает все элементы с датой создания строго меньше, чем у граничного, и\nотсортированные по убыванию этой даты, а вариант с `after` - все элементы с датой создания строго больше, чем у\nграничного, и отсортированные по возрастанию этой даты.\n\n```http request\nGET /chat/messages?per_page=10&before= HTTP/1.1\nHost: api.bestdoctor.ru\nAccept: application/vnd.bestdoctor.v1.verbose+json\n\nHTTP/1.1 200 OK\n{\n  \"data\": [{\"uuid\": \"UUID_FIRST\", ...}, ..., {\"uuid\": \"UUID_LAST\", ...}],\n  \"meta\": {\n    \"total\": 98,\n    \"url\": \"/chat/messages?per_page=10&before=UUID_FIRST\",\n    \"next_url\": \"/chat/messages?per_page=10&before=UUID_LAST\",\n    \"prev_url\": \"/chat/messages?per_page=10&after=UUID_FIRST\"\n  }\n}\n```\n\nЕсли `object_uuid` не задан, то выдается страница отсортированной коллекции и в `meta.url` указывается запрос\nотносительно первого элемента выборки.\n\n### Версионирование API\n\nЗаголовок запроса `Accept` отвечает за выбор версии API, формата ответа, дополнительных опций API.\n\nБазовый content-type для API это `application/json` - формат ответа json, кодировка UTF-8, все дополнительные опции и\nверсия API в дефолте.\n\nРасширенный content-type выглядит так: `application/vnd.bestdoctor.[version].[option[-verbose]][+format]`\n\nПараметр `format` всегда равен `json`.\n\nOption позволяет выбрать различные варианты формирования ответа, передается в конкретный обработчик и может\nинтерпретироваться по-разному:\n\n* `compact` - например, не подставлять связанные объекты, а оставлять их ссылками, либо не включать в выдачу\n  дополнительные параметры\n* `full` - включать в ответ все связанные объекты, дополнительные параметры\n* `verbose` - включать в ответ исходный query или расширенную информацию в meta, красиво форматировать json для\n  разработчика.\n\n## Ресурсы\n\n### Пациенты\n\n#### Список разрешений\n\n* `patient:view` - просмотр информации о пациенте\n* …\n\n#### Получить список пациентов\n\n`GET /patients` - список пациентов, pageable\n\nПараметры:\n\n* `page`, `per_page` - настройки паджинации\n* `qs` - строка поиска\n\nФормат ответа:\n\n```text\n{\n  \"data\": [\n    {\n      \"url\": url пациента,\n      \"patient_id\": id пациента, обязательное поле,\n      …\n      \"appeals_url\": url списка обращений,\n      \"permissions\": [\"patient:view\", \"appeal:add_for_patient\"]\n    },\n    …\n  ]\n}\n```\n\n#### Создать пациента\n\n`POST /patients`\n\nПараметры: ...\n\n#### Получить детальную информацию о пациенте\n\n`GET /patients/:patient_id`\n\nФормат ответа:\n\n```text\n{\n  \"data\": {\n    \"url\": url пациента,\n    \"patient_id\": id пациента, обязательное поле,\n    …\n    \"program\": {\n       \"url\": url на программу, может отсутствовать,\n       …\n    },\n    \"slot\": {\n      \"url\": url на слот, может отсутствовать,\n      …\n    },\n    \"appeals_url\": \"/appeals/?patient=uuid\" - url списка обращений,\n    \"permissions\": [\"patient:view\", \"appeal:add_for_patient\"]\n  },\n}\n```\n\n#### Изменить информацию о пациенте\n\n`PATCH /patients/:patient_id`\n\n#### Удалить пациента\n\n`DELETE /patients/:patient_id`\n\n#### Создать обращение (связанный объект) по пациенту\n\n`POST /patients/:patient_id/appeals`\n\n#### Получить список обращений по пациенту\n\n`GET /patients/:patient_id/appeals`\n\n### Обращения\n\n#### Список разрешений\n\n* `appeal:add_for_patient` - создание обращения для конкретного пациента\n* `appeal:add` - создание обращения\n\n#### Получить список пациентов\n\n`GET /appeals` - список пациентов, pageable\n\nПараметры:\n\n* `page`, `per_page` - настройки паджинации\n* `qs` - строка поиска\n* `patient` - uuid пациента для фильтрации\n\n#### Создать обращение\n\n`POST /appeals`\n\n#### Получить детальную информацию по обращению\n\n`GET /appeals/:appeal_id`\n\n#### Обновить статус обращения\n\n`PUT /appeals/:appeal_id/resolution`\n\n#### Удалить статус обращения\n\n`DELETE /appeals/:appeal_id/resolution`\n\n#### Привязать обращение к пациенту\n\n`PUT /appeals/:appeal_id/patient`\n\n#### Получить список возможных результатов обращений\n\n`GET /appeals_results`\n\nФормат ответа:\n\n```text\n{\n  \"data\": [\n    {\n      \"id\": 4,  // id результата\n      \"text\": \"наименование\"\n    },\n  ]\n}\n```\n\n#### Получить список категорий обращений\n\n`GET /appeals_categories`\n\nФормат ответа:\n\n```text\n{\n  \"data\": [\n    {\n      \"id\": 12,  // id категории\n      \"parent\": 4, // nullable ссылка на родительскую категорию,\n      \"text\": \"Жалобы\"\n    },\n  ]\n}\n```\n"
  },
  {
    "path": "guides/commit_messages_guide.md",
    "content": "# Сообщения коммитов\n\nСообщение коммита – невероятно важная вещь, так как зачастую это единственное место,\nгде можно найти контекст, в котором были произведены изменения кода.\n\n---\n\n## Писать сообщения на английском языке\n\nЕсли нет уверенности в своих способностях, можно выразиться проще или воспользоваться переводчиком.\n\n### Зачем?\n- Чтобы соблюдать правило хорошего тона\n- Чтобы не скатываться к перемешиванию непонятных англицизмов с русским языком\n- Английский язык — фактически язык любого разработчика. Профессия обязывает его знать.\n\n### Хорошо\n\n```\n3cf33864e2 add tests\n136e39ff28 add base logic\n```\n\n### Плохо\n\n```\n3cf33864e2 добавить тесты\n136e39ff28 негізгі логиканы қосты\n```\n\n---\n\n## Начинать сообщение с заглавной буквы\n\n### Зачем?\n\n- Чтобы текст был читабельнее\n- Грамматику никто не отменял. Это мелочь, но внимание к мелочам приближает нас\nк совершенству, а не отдаляет от него. Правила стоит нарушать, если от этого есть толк.\n\n### Хорошо\n\n```\n0bb3518daf BTB-288: Add basic authorization\n```\n\n### Плохо\n\n```\n0bb3518daf BTB-288: add basic authorization\n```\n\n---\n\n## Использовать глаголы в повелительном наклонении\n\nПовелительное наклонение означает «устное или письменное распоряжение, команда\nили инструкция». Например: _add_, _update_, _refactor_ и т.д.\n\nЕсть простое правило для написания сообщений в этом стиле:\n\n    Правильно сформированная тема коммита Git должна всегда иметь возможность завершить следующее предложение:\n\n    If applied, this commit will <your message>\n\n### Зачем?\n\n- Сам git использует такой формат, например, когда создает коммиты слияния\n- Коммиты можно рассматривать как команды\n(например, вид лога и команда revert начинают играть новыми красками)\n\n### Хорошо\n\n```\n3cf33864e2 BTB-501: Add integration tests for certificates\naf932688a0 BTB-501: Attach certificate logic to the registry import order handler\n0bb3518daf BTB-501: Add facade to mark certificate as ready for activation\n```\n\n### Плохо\n\n```\n3cf33864e2 BTB-501: Integration tests for certificates\naf932688a0 BTB-501: Attaching certificate logic\n0bb3518daf BTB-501: Added facade to mark certificate as ready for activation\n```\n\n---\n\n## Связывать коммит с задачей\n\nОбязательно использовать формат сообщения `<ticket>: <message>`.\n\nВо избежание нарушения этого правила рекомендуется настроить прекоммит-хук,\nкоторый проверяет сообщение коммита.\n\nИногда бывают ситуации, когда нужно закоммитить код без привязки к задаче,\nнапример, хотфикс или выпил флага (хотя, в этих случаях тоже рекомендуется заводить задачи).\nТогда можно использовать заглушку с числом `100500` вместо номера тикета.\nЕсли тикеты привязаны к командам или проектам (в случае `Jira` или `Яндекс.Трекер`),\nто следует выбирать очередь своей команды.\nЕсли же это вообще не имеет никакого отношения к продукту,\nможно использовать название инжиниринговой очереди (в случае нашей компании `BANG`).\n\nК тому же, GitLab умеют превращать номер задачи в коммите в ссылку на задачу в трекере.\n\n### Зачем?\n\nСитуация: разработчик приходит исправлять ошибку, находит подозрительное место,\nа сообщение коммита, в рамках которого была исправлена строка, выглядит как `add new selector`.\nЭто, конечно, фиаско, и в этом случае единственная надежда на\nхорошее описание задачи в трекере задач, по которому можно будет понять,\nпочему был написан этот код и какая должна быть логика на самом деле.\n\nЭто особенно актуально в нашем случае, когда автоматизация код-ревью\nзаточена под номер задачи.\n\n_Для пользователей PyCharm_: существует способ настроить встроенную консоль гита\nтак, чтобы номер тикета сразу оформлялся как ссылка на трекер.\n\n### Хорошо\n```\n0bb3518daf BTB-100500: Refactor slot__detach interactor\n```\n\n### Плохо\n\n```\naf932688a0 Add Phone Number Pydantic type, using google's phonenumbers\n0bb3518daf NOISSUE-100500: Refactor slot__detach interactor function\n```\n\n---\n\n## Сначала надо ответить на вопрос «Зачем?»\n\nВ первую очередь при написании сообщения коммита стоит задуматься о том,\nзачем вообще был сделан этот коммит? А ответ записать в сообщение.\n\n### Зачем?\n\nПо диффам всегда видно, что поменялось.\nА вот зачем были внесены изменения, порой решительно непонятно.\nОднажды придется исправлять этот код, и можно сэкономить много времени в будущем,\nесли написать правильное сообщение сейчас.\n\n### Хорошо\n```\naf932688a0 BTB-501: Attach certificate logic to the registry import order handler\n0bb3518daf BTB-501: Add facade to mark certificate as ready for activation\ncdb4629fqc BTB-501: Fix bug when phone number saves to database incorrectly\n```\n\n### Плохо\n```\naf932688a0 BTB-501: Add new logic\n0bb3518daf BTB-501: Add facade\n```\n\n---\n\n## Не стоит смешивать изменения\n\nЭто в большей степени относится к культуре создания коммитов,\nно сообщения являются отличным маркером.\nЕсли не получается и емко описать, что происходит в этом коммите\n(или еще хуже, приходится в теле сообщения написать список изменений),\nстоит декомпозировать изменения и закоммитить код\nв нескольких последовательных коммитах вместо одного большого.\n\nИногда лучше сделать 10 маленьких коммитов, чем один большой,\nв котором собрано все, что связано с задачей.\n\n**Главное правило**: не коммитить изменения бизнес-логики при написании тестов.\nБывает, что в разных коммитах добавляются модели, потом логика, потом тесты.\nХочется избежать ситуаций, когда в такие коммиты подмешиваются изменения бизнес-логики.\nПри нахождении бага лучше зафиксировать изменения отдельным коммитом, а потом вернуться к тестам.\n\nОднако, если коммит содержит большой объем кода,\nполноценно закрывающий функциональность (т.е. это вся бизнес-логика и тесты, например),\nдопустимо закоммитить все вместе.\nНо тут срабатывает правило «попробуй кратко и емко описать, что происходит в этом коммите».\n\nЕсли кратко описать не получается, вместо того, чтобы писать общее сообщение,\nстоит подумать, что в этом коммите самое важное?\nПосле этого подумать, допустимо ли смешивать изменения.\nИногда ответ может быть положительный, иногда отрицательный.\n\n### Зачем?\n\nЧтобы иметь возможность кратко и емко описать, зачем нужен этот коммит.\nК тому же, следовать многим другим рекомендациям невозможно без выполнения этого условия.\n\n## Хорошо\n```\n\n# закоммитили разные учатки кода в разных коммитах\n\n3cf33864e2 BTB-288: Fix program annotation\n    programs/facades.py\n\naf932688a0 BTB-288: Add certificate activation via Tilda\n  certificates/facades.py\n\n0bb3518daf BTB-288: Add unit tests for certificate activation\n  certificates/tests/test_facades.py\n```\n\nили\n\n```\n# закоммитили всю фичу целиком\n\n3cf33864e2 BTB-288: Add certificates activation via Tilda\n\n  programs/facades.py\n  certificates/facades.py\n  certificates/tests/test_facades.py\n```\n\n### Плохо\n\n```\n# непонятное сообщение, при этом затронуто очень много приложений\n\n3cf33864e2 BTB-288: Add logic\n\n  appeals/facades.py\n  programs/facades.py\n  certificates/facades.py\n  certificates/tests/test_facades.py\n  settings.py\n```\n\nили\n\n```\n# в сообщении про тесты, но среди файлов бизнес-логика\n\n3cf33864e2 BTB-288: Add integration tests\n\n  certificates/facades.py\n  certificates/tests/test_facades.py\n```\n\n---\n\n## Писать осмысленные сообщения\n\nИногда хочется написать «исправил баг» или «исправления после код-ревью».\nЛучше не поддаваться этому желанию, а постараться максимально четко и ясно написать,\nчто и зачем было сделано.\n\n### Зачем?\n\nВсе затем же. Чтобы однажды иметь возможность понять, зачем было сделано то или иное изменение.\nК тому же, такой подход заставит задуматься, а надо ли все исправления по код-ревью пихать в один коммит.\nМожет, не стоит?\n\n### Хорошо\n\n```\n3cf33864e2 BTB-501: Fix typo in error message\naf932688a0 BTB-501: Fix duplication of requests in annotation\n0bb3518daf BTB-501: Add logic for manual certificate activation\n```\n\n**Плохо**\n\n```\n3cf33864e2 BTB-501: CR fixes\naf932688a0 BTB-501: Fix error\n0bb3518daf BTB-501: Add logic\n```\n\n---\n\n## Сообщения должны занимать одну строку\n\nНе надо писать длинные сообщения на много строк.\nНе надо пытаться описать в сообщении всю бизнес-логику или скопировать текст связанной задачи в трекере.\n\n**Важно!** В некоторых случаях допустимо написать в теле большое сообщение,\nв котором можно подробно описать проблему и способ её решения, но это бывает редко.\nДля полноты контекста обычно достаточно ссылки на тикет и короткого сообщения.\n\n## Зачем?\n\n- (Конкретно в нашем случае) автоматизация код-ревью не заточена под многострочные коммиты\n- GitLab не очень хорошо отображает многострочные коммиты и сворачивает их по умолчанию\n- Если хочется написать многострочное сообщение, стоит задуматься,\nсоответствует ли коммит описанным выше рекомендациям.\n\n### Хорошо\n\n```\n3cf33864e2 BTB-288: Fix program annotation\n```\n\n### Плохо\n\n```\n3cf33864e2 BTB-288: Fix programs\n  - fix selectors\n  - fix templates\n  - fix facades\n```\n\n---\n\n## Не забывать про amend во время локальной разработки\n\nОпция `--amend` позволяет закоммитить изменения в рамках предыдущего коммита,\nс возможностью переопределить сообщение.\n\n**Важно!**\nКак только код в общедоступном репозитории, использовать эту опцию нельзя;\n`--amend` работает только локально, на коммитах, которые не запушены.\nНарушение этой рекомендации может привести к рассинхронизации ветвей репозитория.\n\n### Зачем?\n\nЧтобы не генерировать мусорные коммиты.\n\n### Хорошо\n```\n3cf33864e2 BTB-501: Add logic for manual certificate activation\n\n  certificates/facades.py\n  certificates/selectors.py\n  certificates/interactors.py\n```\n### Плохо\n```\n3cf33864e2 BTB-501: Add logic for manual certificate activation\n  certificates/facades.py\n\naf932688a0 BTB-501: Add logic for manual certificate activation\n  certificates/selectors.py\n  certificates/interactors.py\n```\n\n---\n\n## Не ставить точку в конце строки\n\n### Зачем?\n\nА зачем ставить? 🙃\n\n### Хорошо\n```\n3cf33864e2 BTB-288: Fix program annotation\n```\n### Плохо\n```\n3cf33864e2 BTB-288: Fix program annotation.\n```\n\n---\n\nПолезные статьи:\n- [Как писать сообщения коммитов в Git [Medium]](https://medium.com/grisme/как-писать-сообщения-коммитов-в-git-9ed19ebc5ebf) + [оригинал](https://cbea.ms/git-commit/)\n- [Как оформлять коммиты, чтобы потом не было больно [Хабр]](https://habr.com/ru/companies/Voximplant/articles/276695/)\n"
  },
  {
    "path": "guides/en/python_styleguide.md",
    "content": "# BestDoctor Python styleguide\n\n## Goals\n\nWe are developing a huge product with tons of functionality. Moreover, we need to maintain and improve our existing codebase.\n\nIn order to make this process as painless as possible, the code needs to be\nflexible, loosely coupled, comply with DRY and KISS principles...\nThe list goes on. Otherwise, developers, technical support or the business\nitself will get an unpleasant surprise later down the line.\nAnd you want to avoid that.\n\nIn order to get the code that meets our standards,\nwe need to constantly keep our focus. That in itself can be quite challenging\nas it requires a lot more awareness than simply writing code.\nWe made this styleguide, so it can help us keep track of our solutions\nto most common problems, instead of having to reinvent the wheel each\ntime one of those problems comes up.\n\nThis guide contains no rules, only guidelines. We follow these\nguidelines in order to write amazing code and not just\nfor the sake of following them.\n\nIt is also very important to us that our code satisfies our team.\nThat's why the guidelines below represent our personal preferences\nthat may not align with yours.\n\n## You are not required to follow these guidelines\n\nAs a matter of fact, in some cases it is better not to follow this styleguide.\nThere are situations when it is okay to write `import *`, `except Exception`,\nuse tab indents or write really long lines of code.\nYou can do that if you are absolutely certain that it will be beneficial\nto your codebase. However, those situations are quite rare.\n\n## The latest Python release\n\nUse 3.9 in production. Use 3.9 in staging. Use 3.9 on every instance.\nThink in 3.9 as well.\n\n## PEP8\n\nWe love and follow PEP8 except for the maximum line length.\nWe have set our upper-limit to 120 characters.\n\nWe use [black](https://github.com/psf/black) with single quotes.\n\n## Comments\n\n### No unnecessary comments\n\nThere are rules that require writing comments in certain places,\ne.g. public modules, classes, methods.\n\nWe don't have such rules. We think that you should only write a comment\nwhen something in your code acts in an unexpected manner.\n\nIf you can't figure out the purpose of an entity, then you should\nconsider renaming it. Maybe it will help clear things up.\n\n### Oneline docstrings\n\nA quick way to write a docstring which can fit in one line. Keep the comment\nand the quotation marks on the same line and don't forget a full-stop.\n\nBad:\n\n```python\n\"\"\"\nStatistics on clients' legal entities.\n\"\"\"\n```\n\nBad:\n\n```python\n\"\"\"Statistics on clients' legal entities\"\"\"\n```\n\nGood:\n\n```python\n\"\"\"Statistics on clients' legal entities.\"\"\"\n```\n\n### Multiline docstrings\n\nUsed when you need to provide a description which exceeds one line.\nHere's how it works: the first line should be used to describe\nwhat you are talking about. It ends with a full-stop.\nThe first line and the description are separated by an empty line.\nThere should be no spaces between the text and the quotation marks.\n\nBad:\n\n```python\n\"\"\"\nActive service period.\nFunctions as inteded when user has one or less slots\n\"\"\"\n```\n\nBad (missing a full-stop):\n\n```python\n\"\"\"\nActive service period\n\nFunctions as inteded when user has one or less slots\n\"\"\"\n```\n\nGood:\n\n```python\n\"\"\"\nActive service period.\n\nFunctions as inteded when user has one or less slots\n\"\"\"\n```\n\n### Inline comments\n\nYou can and you should use them when you need to describe tricky logic,\nrefer to a ticket or to a wiki article.\n\nYou shouldn't use them to make jokes, to describe a certain part of the code or\nto divide it into sections. Leave the first two for the water cooler\nconversations and the last one – for code refactoring.\n\nBad:\n\n```python\n# this is shit\n```\n\nBad:\n\n```python\n# fetching user data\n...\n# building a context for the report\n...\n# generating the report\n...\n```\n\nGood:\n\n```python\n# refer to BES-482 for the formula explanations\n```\n\n### TODO and FIXME\n\nWe use TODO and FIXME to mark something important when we don't want to get\ndistracted while writing code. We try to remove them before making a commit.\nInstead of keeping those marks, we prefer to either fix those parts\nimmediately or create a ticket to do it later.\n\nThere are two reasons for doing so. One is that those marks can stay\nin the code for years and no one does anything about it.\nAnd two is that they make an impression that everything is broken.\n\nKeep your code improvement and refactoring tasks in the task tracker,\nnot in your code.\n\n### Inline comments and urls\n\nLeaving a link without a note in an inline comment is a bad idea.\nIt's better to write a description because it will help others get the context\nwithout having to follow the link.\n\nBad:\n\n```python\n# https://stackoverflow.com/q/184618/3694363\n```\n\nGood:\n\n```python\n# empties the cache properly: https://stackoverflow.com/q/184618/3694363\n```\n\n## String literals and formatting\n\nUse single quotes for string literals, but avoid using backslashes in strings.\n\nTo format a string, use `.format` or f-strings with indexed or\nnamed placeholders. Do not concatenate strings with `+` operator.\n\nF-strings can be used, but you should avoid putting a lot of logic\ninside of them.\n\nBad:\n\n```python\n'%s %s' % (first_name, last_name)\n```\n\nBad:\n\n```python\n'{} {}'.format(first_name, last_name)\n```\n\nGood:\n\n```python\n'{0} {1}'.format(first_name, last_name)\n```\n\n## Imports\n\nWe use [isort](https://github.com/PyCQA/isort).\n\nThird-party libraries should be imported on a case by case basis,\ni.e. import only the functions and classes that you need.\nDo not import the entire module and for the love of god do not use `import *`.\nResolve name collisions with `as` statement.\n\nStandard library modules should be imported in their entirety.\n\nImports should be divided into three sections as described in PEP8.\nThere are no set rules for sorting the imports inside a section.\n\nBad:\n\n```python\nfrom collections import Counter\n```\n\nBad:\n\n```python\nfrom core import models\n```\n\nGood:\n\n```python\nimport collections\nfrom core.models import Patient\n```\n\n## Code inside `__init__.py`\n\n`__init__.py` should only contain imports and nothing else.\n\n## Calls formatting\n\nDo not use line breaks if a call and all of its' arguments can\nfit into a single line.\n\nIf one line is not enough, but you can fit it in the next line\nafter the first bracket, then you should do that.\n\nIf there are too many arguments or you can't fit a call into two lines,\nthen use advanced formatting. In that case, you can place several arguments\nin the same line if they have similar meaning. For instance, you can combine\n`date_from` and `date_to` or `to` and `on_delete`.\n\nBad:\n\n```python\nrecieved_date = models.DateField(\n    'Receival date', auto_now_add=True,\n)  # everything fits into a sinlge line, no need to break it\n\nlegal = models.ForeignKey(\n    'bestdoctor.ClinicLegalEntity',\n    models.CASCADE,\n    'registries', 'registry',\n    verbose_name='clinic legal entity',\n    )  # closing paranthesis has excess indent\n```\n\nGood:\n\n```python\nrecieved_date = models.DateField(\n    'Recieval date', auto_now_add=True)\n\nlegal = models.ForeignKey(\n    'bestdoctor.ClinicLegalEntity',\n    models.CASCADE,\n    'registries', 'registry',\n    verbose_name='clinic legal entity',\n)\n```\n\nGood:\n\n```python\nlegal = models.ForeignKey(\n    'bestdoctor.ClinicLegalEntity',\n    models.CASCADE,\n    'registries',\n    'registry',\n    verbose_name='clinic legal entity',\n)\n```\n\n## Function calls and arguments\n\nWhen calling a function, specify argument names if they are unclear from\nthe name of the function alone.\nYou may write them even if they are obvious.\n\nBad:\n\n```python\nserializer.is_valid(True)\n```\n\nGood:\n\n```python\nserializer.is_valid(raise_exception=True)\n```\n\n## Commas\n\nPlace a comma after the last item in any kind of iterable.\nThis includes lists, calls, tuples or anything similar.\n\nIf it fits in one line then we don't use a comma.\nThe only exception is a one element `set`.\n\nBad (forgot a comma in the end):\n\n```python\nlist_filter = (\n    'legal', 'is_finished', 'is_paid', 'client_registry__record__report_date'\n)\n```\n\nBad:\n\n```python\nAct.objects.create(\n    date=today(),\n    act_type='tech',\n    report_date=report_date,\n    is_finished=False\n)\n```\n\nGood:\n\n```python\nlist_filter = (\n    'legal', 'is_finished', 'is_paid', 'client_registry__record__report_date',\n)\n\nAct.objects.create(\n    date=today(),\n    act_type='tech',\n    report_date=report_date,\n    is_finished=False,\n)\n```\n\n## Unused code\n\nWe want as few bugs as possible in our code. A great way to get rid of bugs is\nto get rid of code. No code means no problems. That's why we remove everything\nthat can be removed, such as deprecated features, one-time scripts,\npieces of code which have been commented out. All of it gets cut.\n\nIt's not difficult to recover it later. Since all commits are tied to a ticket\nin the task manager, to find removed code you need to first find the ticket,\nthen its commits and then reapply them.\n\nIf there is something that you don't need this month then cut it.\n\n## Data in settings\n\nWe are doing our best to keep all of the data where it belongs.\nProject settings is definitely not the right place for it.\nHere's an example of how to move a piece of data from the settings to the\ndatabase with the help of `BooleanField`:\n\nBefore:\n\n```python\n# settings.py\nNOTIFY_IN_SLACK_COMPANY_IDS = [123, 456, 789]\n\n# code\nsend_slack_notification(NOTIFY_IN_SLACK_COMPANY_IDS)\n```\n\nAfter:\n\n```python\n# Companies now have notify_in_slack checkbox\nnotify_in_slack_company_ids = Company.objects.filter(notify_in_slack=True).value_list('id', flat=True)\nsend_slack_notification(notify_in_slack_company_ids)\n```\n\n## Rules for fields in Django models\n\n1. The order of fields is defined in\n  [flake8-class-attributes-order](https://github.com/best-doctor/flake8-class-attributes-order).\n1. `DateTimeField` names should end with `_at`.\n1. `DateField` names should end with `_date`.\n1. For `models.CharField` we use `max_length=255`.\n1. If a field has `choices` attribute, then it should be implemented using `django.db.models.TextChoices` or `django.db.models.IntegerChoices`.\n1. Define `__str__` method for each model. Make sure that it does not generate a lot of database queries.\n1. Every `ForeignKey` field must have a defined `related_name`.\n1. Use `.pk` instead of `.id` to get the model identificator.\n\n## Rules for Django urls\n\n1. Don't use decorators inside an app's `urls.py`. Use  `method_decorator` in\n   Class-Based Views for methods defined for their respective HTTP verbs.\n   More info about this decorator can be found in\n  [oficial Django docs](https://docs.djangoproject.com/en/2.2/topics/class-based-views/intro/#decorating-the-class).\n\n\n## Queue naming convention\nThe main idea is to have queue name prefixed with consuming service name.\n\nSuppose we have three services: `tinker`, `tailor`, `soldier` [, `spy`].\n\nA queue, which is populated and consumed by `tinker` itself, would be called `tinker-default`.\nHaving need for multiple queues,\none might distinct them with qualifiers like `{consuming service name}-{qual1}-{qualN}`, ex:\n`tinker-heavy`, `tinker-light-long`, etc.\n\nAs for service interaction, `{consuming service name}-rpc` looks good.\nFor example, a queue, which is used by `tinker` to send data to `soldier`,\nwould be named `soldier-rpc`.\nNote, that the name has to be the same on the both parties (`tinker`, `soldier`).\n"
  },
  {
    "path": "guides/frontend-styleguide-luchi.md",
    "content": "# Frontend Styleguide LUCHI\n\n> Этот документ - primary source of truth для frontend-стандартов ЛУЧИ.\n>\n> Связанный skill: [frontend-styleguide-luchi](https://bestdoctor.gitlab.yandexcloud.net/bestdoctor/common/skills/-/blob/main/frontend-styleguide-luchi/SKILL.md?ref_type=heads)\n>\n> Если skill неудобен, неполон или устарел, меняем этот гайд или процесс генерации skill, но не редактируем производный skill вручную.\n\n## Базовые принципы\n\n- Единообразие важнее локальных предпочтений.\n- Понятность важнее краткости, cleverness, микрооптимизаций и комментариев.\n- Максимум проверок выносим в инструменты: TypeScript, ESLint, Prettier, tests.\n- Имена отражают бизнес-смысл, а не визуальную реализацию.\n- Пользователь всегда получает feedback на `loading`, `empty`, `error`, `success`.\n\n## Tooling\n\n### Linting\n\n- [`eslint-config-bestdoctor`](https://github.com/best-doctor/eslint-config-bestdoctor) считаем legacy-базой.\n- Для новых проектов нужен новый, более простой и достаточный конфиг.\n- `TODO`: разработать, согласовать и внедрить новый базовый ESLint-конфиг для новых frontend-проектов.\n- Проектные правила и плагины допустимы, если они не ломают единый стиль.\n- Легаси-нарушения временно переводим в `warn`; по мере рефакторинга исключения убираем.\n- Отключение правила линтинга:\n    - только точечно;\n    - только с объяснением причины;\n    - по возможности со ссылкой на задачу.\n\n### Formatting\n\n```json\n{\n    \"printWidth\": 120,\n    \"semi\": true,\n    \"singleQuote\": true,\n    \"tabWidth\": 2,\n    \"trailingComma\": \"es5\",\n    \"bracketSpacing\": true,\n    \"arrowParens\": \"always\",\n    \"endOfLine\": \"lf\"\n}\n```\n\n- Конфиг задается в корневом `.prettierrc`.\n- Если проектный конфиг отсутствует, создаем его в этом виде.\n- Используем `eslint-config-prettier`, чтобы не конфликтовать с ESLint-конфигом.\n- Локальные отклонения от этого конфига допустимы только по явной проектной причине.\n\n### Styling\n\n- Следуем существующему проектному стеку.\n- По умолчанию предпочитаем `Tailwind CSS` или `CSS Modules`.\n- Не добавляем новый styling-подход без явной причины.\n\n## Архитектура\n\nИспользуем FSD-lite.\n\n### Слои\n\n- `src/app` - providers, app-level config, global styles.\n- `src/pages` - route-level composition.\n- `src/entities` - бизнес-сущности; основной слой предметной логики и UI вокруг сущностей.\n- `src/shared` - переиспользуемые UI-компоненты, общие utils, constants, types и инфраструктурные модули.\n- `src/widgets` и `src/features` - опциональны; добавляются только если проект реально выигрывает от отдельного orchestration-слоя.\n\n### Структура slice\n\nВнутри slice используем директории по назначению:\n\n- `ui` - view-слой.\n- `hooks` - hooks и slice-level logic.\n- `stores` - локальные и slice-level stores.\n- `utils` - локальные утилиты.\n- `constants` - константы slice.\n- `types` - типы slice.\n\n`pages` внутри `entities` не создаем. Глобальные singleton-like вещи размещаем в `app` или `shared`, а не в отдельном top-level `services`.\n\n### Границы импортов\n\n- Верхний слой может импортировать нижний.\n- Импорты между slice одного слоя запрещены.\n- Циклические зависимости запрещены.\n- `index.ts`-реэкспорты по умолчанию запрещены.\n- Barrel-файлы допустимы только как осознанный public API, если это не ухудшает tree-shaking, bundle size и поиск мертвого кода.\n\n## Данные и стейт\n\n### Backend и запросы\n\nИспользуем `@tanstack/react-query`.\n\nПеред тем как писать ручной клиент или query factory:\n\n1. Проверяем, нет ли уже автогенеренных клиентов, hooks или готового инфраструктурного слоя для работы с backend.\n2. Проверяем project-level инструкции (`AGENTS.md`, `.cursorrules`, `README`).\n3. Только если готового решения нет, пишем ручную реализацию.\n\n### Query keys\n\nДля ручной реализации используем key factory:\n\n```ts\nexport function getRequestsKeys<TListParams, TItemKey = string>(\n    entityName: string,\n) {\n    return {\n        all: [entityName] as const,\n        lists: [entityName, \"list\"] as const,\n        list: (params: TListParams) => [entityName, \"list\", params] as const,\n        items: [entityName, \"item\"] as const,\n        item: (key: TItemKey) => [entityName, \"item\", key] as const,\n    };\n}\n```\n\n- `all` - все запросы сущности.\n- `lists` - все списки.\n- `list(params)` - конкретный список.\n- `items` - все item-запросы.\n- `item(key)` - конкретная запись.\n\n### Query hooks\n\n- Используем object API: `useQuery({ ... })`, `useInfiniteQuery({ ... })`, `useMutation({ ... })`.\n- Хуки должны принимать `options`, если это не делает API хуже.\n- `staleTime` и invalidation определяем по характеру данных, а не по привычке.\n- На `mutation` по умолчанию инвалидируем релевантные `lists` и `item(id)`.\n\n### Нейминг query/mutation hooks\n\n- Одна запись: `useUserQuery`\n- Специфический запрос: `useUserByEmailQuery`\n- Список: `useUsersListQuery`\n- Бесконечный список: `useInfiniteUsersListQuery`\n- Бесконечный поиск: `useInfiniteSearchUsersQuery`\n- Создание: `useCreateUserMutation`\n- Обновление записи: `useUpdateUserMutation`\n- Обновление поля: `useUpdateUserNameMutation`\n- Удаление: `useRemoveUserMutation`\n\n### Формы и локальный стейт\n\n- Для форм используем `react-hook-form`.\n- Все, что может быть локальным, оставляем локальным.\n- Глобальный стейт - исключение, а не норма.\n- Глобальный store используем только для реально shared-состояния: авторизация, глобальный виджет, cross-page state.\n\n### Loading / Empty / Error\n\n- Предпочитаем `Suspense + ErrorBoundary + Skeletons`.\n- Не оставляем пользователя на пустом экране.\n- Если данных нет - показываем empty state.\n- Если запрос упал - показываем понятный error state и следующий шаг.\n\n## Нейминг\n\n### Общие правила\n\n- Используем английский язык.\n- Название должно быть семантичным и бизнес-ориентированным.\n- Понятность важнее длины и \"красоты\".\n- Называем сущность по роли, а не по иконке, цвету или layout.\n\nХорошо: `TogglePasswordVisibilityButton`\n\nПлохо: `EyeIconButton`\n\n### Функции\n\n- Имя функции начинается с глагола.\n- `get*` - возвращает значение.\n- `validate*`, `check*` - возвращает `boolean`.\n- `format*` - преобразует данные.\n- `set*` - пишет в state/store/ref.\n- `handle*` - внутренний handler.\n- `on*` - callback prop компонента.\n\n### Переменные\n\n- Переменная - существительное.\n- Boolean-флаги: `is*`, `has*`, `can*`, `should*`, `was*`, `did*`.\n- Коллекции называем во множественном числе: `users`, `errors`, `phoneNumbers`.\n- Количество сущностей - `*Number`.\n- Порядковый номер - `*Index`.\n- Raw/form payload - `*Data`.\n- Состояние называем как состояние: `isOpen`, `isEnabled`; не смешиваем state и action.\n\n### Компоненты, файлы, директории\n\n- Компоненты - `PascalCase`.\n- Файлы и директории - `camelCase`.\n- Имя файла по возможности совпадает с главным export.\n\n### Naming reference\n\n- Хорошее имя описывает роль в бизнес-логике, а не реализацию.\n- `usersPhoneNumbers` - номера телефонов пользователей.\n- `userPhoneNumbers` - номера телефонов одного пользователя.\n- `usersPhoneNumber` - плохое имя: число сущностей и число коллекции конфликтуют.\n- Для UI-компонентов имя строится от назначения: `SubmitOrderButton`, `UserProfileCard`, `TogglePasswordVisibilityButton`.\n- Имена вроде `BlueCard`, `LeftPanel`, `EyeIconButton` допустимы только если цвет, позиция или иконка и есть бизнес-смысл сущности, что почти никогда не так.\n\n## Дизайн кода\n\n### Single responsibility\n\nОдна функция делает одну задачу и делает ее хорошо.\n\nПризнаки, что функцию пора делить:\n\n- в названии есть `And`;\n- `boolean`-аргумент переключает поведение;\n- функция разрастается на сотни строк;\n- внутри смешаны orchestration, data transform и view logic.\n\n### Параметры функций\n\n- `1-2` аргумента - обычно positional.\n- `3+` аргумента - объект.\n- Один объект тоже допустим, если функция будет расширяться.\n- Исключение: thin wrapper над backend/API может принимать много полей, если функция почти не содержит логики.\n\n### Комментарии\n\n- Комментарии по умолчанию не пишем.\n- Предпочитаем self-documenting code через нейминг и декомпозицию.\n- Допустимы:\n    - причина отключения линтинга;\n    - `TODO` / tech debt с ссылкой на задачу;\n    - краткое объяснение нетривиального решения, которое нельзя сделать очевиднее кодом.\n\n## Reference\n\n### FSD-lite example\n\n```text\nsrc/\n  app/        # providers, app config, global styles\n  pages/      # route-level composition\n  entities/   # User/, Order/, Product/\n  shared/     # ui/, utils/, constants/, types/\n  widgets/    # optional\n  features/   # optional\n```\n\n### Forbidden imports\n\n- `entities/user` -> `entities/order` запрещено.\n- `shared/ui/button` -> `entities/user` запрещено.\n- `pages/userPage` -> `pages/orderPage` обычно запрещено; общую логику выносим ниже.\n- Если orchestration нужен между несколькими entities, поднимаем его в `pages`, `widgets` или `features`.\n\n### React Query reference\n\n- Предпочитаем generic-обертки с `options`, чтобы хук можно было переиспользовать без копипаста.\n- Для списков используем `list(params)`, для одной сущности `item(id)`.\n- `invalidateQueries` бьет либо по конкретному `item`, либо по набору `lists`; не инвалидируем `all`, если можно сузить область.\n- Если проект использует автогенерацию API-клиента и hooks, придерживаемся project-native pattern, а не насаждаем manual factory поверх него.\n\n## Canonical examples\n\n### Query key factory\n\n```ts\nexport function getRequestsKeys<TListParams, TItemKey = string>(\n    entityName: string,\n) {\n    return {\n        all: [entityName] as const,\n        lists: [entityName, \"list\"] as const,\n        list: (params: TListParams) => [entityName, \"list\", params] as const,\n        items: [entityName, \"item\"] as const,\n        item: (key: TItemKey) => [entityName, \"item\", key] as const,\n    };\n}\n```\n\n### Query hook\n\n```ts\nimport { useQuery, type UseQueryOptions } from \"@tanstack/react-query\";\n\ntype FetchError = { message: string };\ntype UserResponse = { id: string; name: string; email: string };\ntype UserResult = UserResponse;\n\nconst userKeys = getRequestsKeys<never, string>(\"users\");\n\nasync function getUser(id: string): Promise<UserResponse> {\n    const response = await fetch(`/api/users/${id}`);\n\n    if (!response.ok) {\n        throw new Error(\"Network response was not ok\");\n    }\n\n    return response.json();\n}\n\ntype UseUserQueryOptions<TResult> = Partial<\n    UseQueryOptions<UserResponse, FetchError, TResult>\n>;\n\nexport function useUserQuery<TResult = UserResult>(\n    id: string,\n    options?: UseUserQueryOptions<TResult>,\n) {\n    return useQuery<UserResponse, FetchError, TResult>({\n        queryKey: userKeys.item(id),\n        queryFn: () => getUser(id),\n        staleTime: 60_000,\n        ...options,\n    });\n}\n```\n\n### Mutation hook\n\n```ts\nimport {\n    useMutation,\n    useQueryClient,\n    type UseMutationOptions,\n} from \"@tanstack/react-query\";\n\ntype FetchError = { message: string };\ntype UserResponse = { id: string; name: string };\ntype UpdateUserParams = { id: string; name: string };\n\nasync function updateUser(params: UpdateUserParams): Promise<UserResponse> {\n    const response = await fetch(`/api/users/${params.id}`, {\n        method: \"PUT\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify(params),\n    });\n\n    if (!response.ok) {\n        throw new Error(\"Network response was not ok\");\n    }\n\n    return response.json();\n}\n\nexport function useUpdateUserMutation(\n    options?: UseMutationOptions<UserResponse, FetchError, UpdateUserParams>,\n) {\n    const queryClient = useQueryClient();\n\n    return useMutation({\n        mutationFn: updateUser,\n        onSuccess: (data) => {\n            queryClient.invalidateQueries({ queryKey: userKeys.lists });\n            queryClient.invalidateQueries({ queryKey: userKeys.item(data.id) });\n        },\n        ...options,\n    });\n}\n```\n\n### Suspense boundary\n\n```tsx\nimport { Suspense } from \"react\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nexport function UserPage({ userId }: { userId: string }) {\n    return (\n        <ErrorBoundary fallback={<div>Something went wrong</div>}>\n            <Suspense fallback={<div>Loading...</div>}>\n                <UserProfile userId={userId} />\n            </Suspense>\n        </ErrorBoundary>\n    );\n}\n```\n\n## Требования к derived skill\n\n- Derived skill создается из этого guide, а не дописывается вручную.\n- Guide должен содержать не только policy, но и critical reference/examples, которые нужны для корректной генерации кода агентом.\n- Progressive guide должен содержать не только policy, но и нормативную базу для производных материалов.\n- Derived skill по умолчанию строится как progressive disclosure:\n    - `SKILL.md` содержит только quick start, critical defaults и ссылки;\n    - подробности выносятся в отдельные derived files вроде `reference.md`, `examples.md`, `config.md`;\n    - ссылки остаются one-level deep.\n- Skill берет из guide:\n    - core rules;\n    - architecture constraints;\n    - naming rules;\n    - React Query / state rules;\n    - canonical examples, если они нужны агенту для качественной генерации.\n\n## Короткий checklist\n\n- Архитектура укладывается в FSD-lite.\n- Импорты соблюдают границы слоев.\n- Query/mutation hooks названы консистентно.\n- Loading, empty и error states явные.\n- Названия отражают бизнес-смысл.\n- Линт и форматирование не подавлены без причины.\n"
  },
  {
    "path": "guides/guide_skill_workflow.md",
    "content": "# Guide-Skill Workflow\n\nЭтот документ описывает, как вести связку `гайд -> производный skill`.\n\n## Статус модели\n\n- Репозиторий гайдов хранит primary source of truth.\n- Репозиторий skills хранит все skills в ИТ.\n- Если skill основан на гайде, гайд первичен, skill вторичен.\n- Skills без привязки к гайдам существуют самостоятельно и считаются first-class citizen.\n\n## Непереговорные правила\n\n1. Меняем сначала гайд, потом пересоздаем skill.\n2. В guide-derived skill обязательно есть ссылка на первичный гайд.\n3. В гайде обязательно есть ссылка на связанный skill.\n4. Если skill получился неудобным, меняем гайд или процесс генерации, но не правим skill вручную.\n5. Название, терминология и scope guide и derived skill должны совпадать.\n6. Нормативные `reference`, `examples`, `config` и аналогичные материалы должны быть встроены в primary guide, а не жить как единственный источник правил вне него.\n7. Guide-derived skill по умолчанию делаем progressive: в `SKILL.md` только essential operational instructions, а детали выносим в отдельные linked files одного уровня вложенности.\n\n## Workflow для человека\n\n1. Обновить первичный гайд.\n2. Проверить, что текст стал короче, точнее и не потерял обязательные правила, reference-блоки, canonical examples и config snippets.\n3. Если важная часть стандарта живет во внешнем reference/examples/config-файле, перенести ее нормативную часть в guide.\n4. Пересоздать связанный skill из гайда в progressive disclosure-формате:\n    - `SKILL.md` для essential instructions;\n    - `reference.md` / `examples.md` / `config.md` или аналогичные файлы для деталей;\n    - ссылки только на файлы одним уровнем ниже.\n5. Проверить двусторонние ссылки `guide <-> skill`.\n6. Проверить, что resource-файлы не содержат новых правил, которых нет в guide.\n7. Обновить индексы и README, если поменялись имя или путь.\n8. Убедиться, что старый skill-name или старый guide-path не остались в репозитории.\n\n## Workflow для агента\n\n1. Найти первичный гайд и считать его источником правды.\n2. Если задача касается guide-derived skill, сначала править гайд.\n3. После обновления гайда пересоздать skill как краткую operational-версию:\n    - меньше воды;\n    - только agent-relevant instructions;\n    - обязательная ссылка на guide;\n    - без ручных локальных правок поверх гайда;\n    - с progressive disclosure через linked resource files.\n4. В `SKILL.md` держать только короткий quick start, critical defaults и ссылки на ресурсы.\n5. Вынести детали в отдельные файлы вроде `reference.md`, `examples.md`, `config.md`, если они действительно помогают и остаются derived from guide.\n6. Если важные примеры, reference-блоки или config snippets находятся вне guide, сначала встроить их в guide и только потом регенерировать skill.\n7. Если derived skill выглядит слабым, исправлять guide или инструкцию генерации.\n8. Не переносить ответственность за стандарты из guide в skill.\n\n## Checklist синхронизации\n\n- Имя guide и skill согласованы.\n- Guide содержит актуальную ссылку на skill.\n- Skill содержит актуальную ссылку на guide.\n- В skill нет правил, которых нет в guide.\n- В guide нет ключевых правил, потерянных при генерации skill.\n- В guide есть все нормативные fragments из внешних reference/examples/config-материалов.\n- `SKILL.md` остается компактным и не дублирует весь guide.\n- Детали вынесены в linked resource files одного уровня вложенности.\n- README и служебные индексы обновлены.\n"
  },
  {
    "path": "guides/infra_infosec_guide.md",
    "content": "# Гайд по безопасности\n\n## Что это такое\n\nКраткое описание технологий и инструментов, которые мы применяем в работе для обеспечения безопасности Linux хостов и\nсохранности данных пользователей. При выборе инструментов обеспечения безопасности, мы руководствуемся здравым смыслом\nи лучшими практиками доступными сообществу.\n\n## Что мы делаем\n\nМы используем подходы Configuration as code и Infrastructure as code для того чтобы понимать что изменяется в нашей\nинфраструктуре, кем и с какой целью. Для этого мы используем инструменты автоматизации: `ansible`, `gitlab`, `terraform`.\n\nМы хотим тестировать применённую конфигурацию на соответствие разумным требованиям безопасности данных и хостов,\nпредоставляемыми нашим бизнесом, согласовывая их с общепринятыми базовыми требованиями от сообщества.\nПоэтому мы используем инструмент для тестирования конфигурации [Chef Inspec](https://inspec.io) и следим за развитием\nпроекта [DevSec Hardening Framework](https://dev-sec.io/)\n\nМы хотим следить за базами уязвимостей/уязвимых пакетов в используемых дистрибутивах и своевременно обновлять ПО.\nПоэтому мы автоматизировали этот процесс и настроили еженедельную отправку отчётов о состоянии установленных пакетов с\nпомощью [Vuls](https://vuls.io/). После получения отчёта мы применяем патчи/устанавливаем критические обновления по расписанию.\n"
  },
  {
    "path": "guides/opensource_guide.md",
    "content": "# Правила создания и поддержки opensource-проектов в BestDoctor\n\n## Мотивация\n\nСейчас на <https://github.com/best-doctor/> 20+ проектов в разной\nстадии разработки. Мы хотим поддерживать их в нормальном состоянии, чтобы\nприносить больше пользы сообществу и самим себе.\n\nВ этом гайде собраны правила, которых мы стараемся придерживаться при разработке\nи сопровождении наших opensource-проектов и процесс этого сопровождения.\n\n## Процесс сопровождения\n\nОсновной элемент сопровождения – еженедельный чек проектов по этому гайду.\nНа это выделяется 1-2 часа в неделю, чтобы найти новые поломки, ответить на\nвопросы в issues, сделать ревью пул-реквестов, обновить зависимости,\nзачинить билды. Этот процесс нужен не для развития проектов, а для их\nподдержки. Основных целей у этого процесса две: минимизировать количество\nкритичных нарушений этого гайда и максимизировать количество проектов,\nкоторые полностью ему соответствуют.\n\nПроверять проекты на соответствие гайду руками – муторное и неблагодарное\nзанятие, поэтому в наших кузницах уже разрабатывается `opensource_watchman`\n– скрипт, который делает всё сам и показывает список нарушений по каждому\nпроекту. Скоро он будет готов и ссылка на него будет тут.\n\nРазвитие проектов – то, к чему может присоединиться кто угодно, в том числе мы.\nДля этого у нас есть открытые задачи, понятные гайды, CI и правила участия\nв проектах.\n\n## Язык проекта\n\nОсновной язык всех проектов – английский. Он используется везде: в текстах\nкоммитов, комментариях, задачах, при общении в пул-реквестах и в документации.\n\nЭто нужно, чтобы наши проекты могли принести пользу максимальному\nколичеству людей.\n\nИсключений может быть два. Первое – репозитории с текстовой информацией, а не\nкодом (например, этот репо). Такие проекты могут быть на русском. Второе –\nпроекты, актуальные только для русского языка. Пример такого проекта\n(извне BestDoctor, правда) – это\n<https://github.com/Melevir/rozental_as_a_service>. Он работает только с русским\nязыком и его основной язык – русский.\n\nЕсли проект на русском, стоит подумать о его переезде на английский полностью\nили поддержки нескольких языков с процессом актуализации переводов.\n\n## Ридми и документация\n\nУ каждого проекта должен быть ридми с исчерпывающей информацией: что это,\nзачем оно нужно, как работает, как установить, как запустить,\nчто для этого нужно, пример использования, как сделать вклад в проект.\n\nДля унификации основной ридми должен лежать в корне проекта и\nназываться `README.md`.\n\nРидми и другая документация должна валидироваться с помощью `mdl`.\n\nВ ридми должны быть бейджики со статусом билда и процентом покрытия\nкода тестами. Остальные бейджики можно не добавлять.\n\n## Поддерживаемые версии языков\n\nМы стараемся использовать в opensource-проектах те же технологии, которые\nиспользуем в бою.\n\nВне зависимости от выбранного языка, наши проекты должны поддерживать\nминимум две последние минорные версии языка.\n\n## CI и сторонние сервисы\n\nДля унификации мы используем Travis CI для, собственно, CI и Codeclimate\nдля дополнительного статического анализа и подсчёта покрытия тестами.\n\nВ Тревисе должна быть настроена основная таска, которая запускается на пуш\nи прогоняет все проверки (стиль кода, типизация, тесты, mdl,\nsafety и все-все-все) и тесты. Если билд зелёный – значит, все\nпроверки проходят.\n\nКроме этого в Тревисе стоит настроить еженедельную крон-джобу, которая делает\nвсё то же, но без коммита. Это позволяет вовремя отловить обратно несовместимые\nобновления незамороженных зависимостей, найденные новые CVE и прочие ништяки,\nо которых хорошо бы знать как можно раньше.\n\n## Безопасность и зависимости\n\nМы заботимся о том, чтобы наш код не наносил вреда и чтобы с его помощью\nнельзя было нанести вред. Для этого мы действуем по двум направлениям:\nследим за нашим кодом и за нашими зависимостями.\n\nВ нашем коде мы стараемся не допускать распространённых ошибок, связанных с\nбезопасностью. Для хоть какой-то автоматической проверки этого в коде на\nPython мы используем dlint.\n\nДля мониторинга обновлений безопасности в наших зависимостях на Python\nмы используем safety. Он должен быть в билде на пуш и, что самое важное,\nеженедельном билде. Если найдётся уязвимость в одной из зависимостей,\nсломается еженедельный билд и мы об этом узнаем.\n\n## Версионирование\n\nВо всех проектах используем\n[семантическое версионирование](https://semver.org/lang/ru/).\n\n## Политика релизов\n\nКод может лежать на Гитхабе, но не быть зарелижен, не больше 2 недель.\n\nВажно понимать, что 2 недели – это допустимый, но не желательный срок.\nВ идеале код отправляется в релиз сразу после пуша. У нас тут ZDD или как?\n\n## Правило последнего коммита\n\nПоследний коммит в любой проект должен быть меньше, чем 6 месяцев назад.\nЗа это время точно что-то произошло даже если проекта не дорабатывается:\nприехала новая версия языка, обновились зависимости или наши требования\nк нашим проектам.\n\nЕсли мы не планируем дорабатывать проект, это нужно сделать явным:\nнаписать дисклеймер в ридми и заархивировать проекта на Гитхабе.\n\n## Покрытие кода тестами\n\nМинимальное покрытие кода тестами должно составлять 80%.\n\nЭто важно не только для устойчивой дальнейшей разработки проекта, но и для\nуверенности в проекте тех, кто только решает, использовать ли его.\n\nТесты должны гоняться в билде, иначе от них почти нет смысла.\n\nДля достижения этой цели мы используем и юнит-тесты и интеграционные.\n\n## Работа с задачами\n\nУ каждого проекта должно быть 3+ открытых актуальных задачи.\nЭто нужно, чтобы помочь сделать вклад в проект тем, кто этого хочет.\n\nНа свежесозданные задачи мы отвечаем в течение 14 дней.\nНет ничего хуже проекта, мейнтейнер которого не обрабатывает задачи.\n\n## Работа с пул-реквестами\n\nВсе входящие пул-реквесты обрабатываем за 7 дней: делаем ревью и просим\nдоработать или мерджим. Желательно быстрее: если кто-то сделал пул-реквест,\nнам важно сделать его опыт взаимодействия с мейнтейнером как можно более\nбыстрым.\n"
  },
  {
    "path": "guides/performance_review_guide.md",
    "content": "# Правила проведения перфоманс-ревью в BestDoctor\n\nПерфоманс ревью – встреча, которая проводится с каждым сотрудником it-отдела BestDoctor дважды в год.\nНа ней сотрудник и компания синхронизирует взаимные ожидания и, возможно, сотруднику повышают оклад.\n\nКроме основных перфомансов есть ежемесячные мини-перфомансы. Они не привязаны к деньгам, но помогают\nотследить прогресс в выполнении задач, о которых договорились на \"большом\" перфомансе\nи сделать процесс проведения перфоманс-ревью более прозрачным и предсказуемым.\n\nПовышение оклада обычно происходит за уже достигнутые успехи, которые находятся за пределами\nосновных рабочих обязанностей сотрудника. Мы не повышаем оклад разработчику за то, что он выполняет\nзадачи в срок потому, что считаем это частью обязательного плана, за который сотрудник получает\nзарплату. Мы повышаем оклад разработчику за то, что он взял на себя управление командой,\nпредложил и довёл до боя несколько инженерных подпроектов, много выступал на митапах,\nсделал вклад в развитие опенсорса в компании и другие подобные вещи. Все они должны быть\nполезны для компании и не являться обязательными для сотрудника.\n\nНемного подробнее про каждую из встреч.\n\n## Перфоманс-ревью\n\n**Периодичность**: Проводится каждые полгода с момента устройства сотрудника на работу.\n\n**Проводит**: CTO.\n\n**Цели**:\n\n- Найти слабые места.\n- Отметить успехи за прошедшие полгода.\n- Составить план на следующие полгода.\n\n**Пререквизиты**:\n\n- Обратная связь от коллег.\n- План с предыдущего ревью (опционально).\n\n**План беседы**:\n\n- Узнать ожидание сотрудника от встречи.\n- Выдача негатива:\n    - Выдать объективную обратную связь по слабым местам.\n    - Обсудить полученный негатив, насколько он соответствует действительности и внутренним ощущениям человека.\n    - Обсудить готовность работать над исправлением слабых мест.\n- Выдача позитива:\n    - Отметить успехи за прошедшие полгода.\n    - Сообщить соотношение негативного и позитивного и общем впечатлении.\n    Это нужно для более адекватной картины, т.к. негативные и позитивные части могут занимать время,\n    не пропорциональное количеству этого самого негатива и позитива\n- Сбор обратной связи по обратной связи:\n    - Спросить, насколько ожидания от встречи совпали с реальностью.\n    - Узнать про слабые места и успехи с точки зрения сотрудника.\n- Составление плана развития:\n    - Спросить про то, в какой области хочется развиваться.\n    - Уточнить, какое видение компании на рост сотрудника и как это бьётся с его желаниями.\n    - Проверить как план развития бьётся с грейдами.\n\nВ результате ревью оба её участника синхронизированы в ожиданиях и имеют зафиксированный план развития\nсотрудника до следующего перфоманса. Успешное выполнение плана является необходимым требованием для повышения.\n\n## Промежуточное перфоманс-ревью\n\n**Периодичность**: Проводится каждый месяц вместо One-to-One встречи до 6 числа.\n\n**Проводит**: Тимлид для линейных членов команды / CTO для тимлидов и юнитов.\n\n**Цели**:\n\n- Обеспечить более плавный прогресс по плану с перфоманса.\n- Синхронизироваться в ожиданиях от грядущего перфоманса.\n\n**План беседы**:\n\n- Посмотреть в план развития.\n- Оценить прогресс за прошедший месяц:\n    - Спросить сотрудника про его оценку прогресса.\n    - Дать обратную связь по прогрессу.\n- Составить план на следующий месяц.\n\nЕсли встречу проводит тимлид, то он перед встречей валидирует свой фидбек для сотрудника у CTO, чтобы\nрезультаты промежуточных перфомансов не расходились с результатами \"больших\" перфомансов."
  },
  {
    "path": "guides/python_styleguide.md",
    "content": "# Правила написания кода на Python в BestDoctor\n\n## Мотивация\n\nМы разрабатываем здоровенный продукт с кучей функциональности.\nБолее того, нам потом этот код ещё поддерживать и развивать.\n\nДля этого код должен быть гибким, слабо связанным, соблюдать принципы\nDRY, KISS и ещё много чего должен. Иначе в долгосрочной перспективе\n вся лажа вылезет наружу и будет больно разработчикам, бизнесу и поддержке.\n Такой лажи надо избегать.\n\nЧтобы получать код, подходящий нашим требованиям, надо постоянно думать.\nПостоянно думать сложно: выходит медленно и постоянно ошибаешься.\nПоэтому некоторые кусочки думанья, которые касаются кода, мы выносим в\nстайлгайд. Теперь в некоторых случаях можно не думать, а просто пользоваться правилом.\n\nК гайду нужно относиться не как к строгому своду правил, а как к опытному советнику.\nСоблюдаем правила не ради правил, а чтобы получать классный код, отвечающий нашим требованиям.\n\nНам также важно, чтобы код нравился команде разработки. Поэтому наш гайд\nзаточен под участников нашей команды и их вкусы. Вам это может быть не по вкусу.\nЭто нормально. Расслабьтесь. Выдохните. Закажите виски сауэр.\n\n## Можно нарушать\n\nЛюбое правило из этого гайда можно нарушить. Более того, в некоторых\nситуациях не нарушать гайд – это ошибка. Такие ситуации случаются редко,\nи в каждой из них нужно иметь железную аргументацию для нарушения.\n\nНи одно из правил не соблюдаем ради соблюдения правила.\nБывают ситуации, когда можно сделать `import *`, `except Exception`,\nотступы табами и длиннющие строки. Смело это делаем, если уверены, что это улучшит код.\nУстраивать пляски с бубном ради Карго-культа не для нас.\n\n## Последний Python\n\nВ бою 3.9, на стейджинге 3.9, на всех инстансах 3.9,\nдумаем тоже на 3.9.\n\n## PEP8\n\nЛюбим и соблюдаем PEP8, за исключением длины строки. У нас жёсткая граница - 100 символов.\n\nИспользуем [black](https://github.com/psf/black). Кавычки - одинарные.\n\n## Комментарии\n\n### Нет необходимых комментариев\n\nЕсть правила, которые требуют написание комментариев в некоторых местах.\nНапример, у публичных модулей, классов, методов.\n\nУ нас таких правил нет. Комментарии необходимо писать там, где они к месту:\nкогда функция делает что-то неочевидное, обрабатывается хитрый случай.\n\nЕсли из названия сущности непонятно, что она делает, сначала стоит\nпопробовать её переименовать, чтобы было понятно.\n\n### Однострочные докстринги\n\nКороткий способ написать докстринг, который помещается на одной строке.\nПереносить не надо, заканчивать точкой обязательно.\n\nПлохо:\n\n```python\n\"\"\"\nПодсчёт статистики по юрлицам клиентов.\n\"\"\"\n```\n\nПлохо:\n\n```python\n\"\"\"Подсчёт статистики по юрлицам клиентов\"\"\"\n```\n\nХорошо:\n\n```python\n\"\"\"Подсчёт статистики по юрлицам клиентов.\"\"\"\n```\n\n### Многострочные докстринги\n\nНужны, когда есть что сказать и одной строки мало.\nСтруктура такая: первая строка описывает что это такое и заканчивается точкой.\nОтбивается от тела комментария пустой строкой.\nУ комментария нет дополнительных отступов от кавычек.\n\nПлохо:\n\n```python\n\"\"\"\nАктивный период обслуживания.\nРаботает как надо, если у человека не более одного активного слота\n\"\"\"\n```\n\nПлохо (точка потерялась):\n\n```python\n\"\"\"\nАктивный период обслуживания\n\nРаботает как надо, если у человека не более одного активного слота\n\"\"\"\n```\n\nХорошо:\n\n```python\n\"\"\"\nАктивный период обслуживания.\n\nРаботает как надо, если у человека не более одного активного слота.\n\"\"\"\n```\n\n### Многострочный текст\n\nВыравнивание кавычек/скобок как для функций при многострочном вызове;\nтекст пишется со следующей строки c отступом в 4 пробела:\n\nЕсли количество пробелов некритично:\n\n```python\nquery = \"\"\"\n    mutation {{\n      detachSlot({0}) {{\n        slot {{\n          id\n          endDate\n        }}\n      }}\n    }}\n\"\"\"\n```\n\nИначе:\n\n```python\nauthor = 'Георг наш, понимаете ли, Гегель.'\nfragment = (\n    'Абсолютная идея есть для себя, потому что в ней нет ни перехода, '\n    'ни предпосылок и вообще никакой определенности, которые не были бы '\n    'текучи и прозрачны; она есть чистая форма понятия, которая созерцает '\n    'свое содержание как самое себя. Она есть свое собственное содержание, '\n    'поскольку она есть идеальное различение самой себя от себя, и одно из '\n    'этих различий есть тождество с собой, которое, однако, содержит в себе '\n    'тотальность форм как систему содержательных определений. '\n    'Это содержание есть система логического.'\n    f'({author})'\n)\n```\n\nЕсли мультилайн нужен при инстанцировании объекта/вызове функции:\n\n```python\nraise GraphQLError(\n    'Эх, '\n    'Вася-Вася!'   # noqa: C812\n)\n```\n\nИли можно еще так:\n\n```python\nraise GraphQLError((\n    'Эх, '\n    'Вася-Вася!'\n))\n```\n\n### Инлайновые комментарии\n\nМожно и нужно писать, чтобы описать неявный случай, сослаться на\nтикет или вики.\n\nНе пишем, если хотим пошутить, дать коду характеристику или разбить\nкод на логические блоки. Для первых двух пунктов есть кухня,\nдля третьего – рефакторинг кода на более мелкие куски.\n\nПлохо:\n\n```python\n# это какое-то говно\n```\n\nПлохо:\n\n```python\n# получаем данные пользователя\n...\n# формируем контекст для отчёта\n...\n# генерируем отчёт\n...\n```\n\nХорошо:\n\n```python\n# подробнее о формулах можно почитать в BES-482\n```\n\n### Комменты TODO и FIXME\n\nПишем комменты TODO и FIXME, когда вспоминаем что-то важное\nв процессе работы над фичей и не хотим отвлекаться,\nно перед коммитом всё вычищаем: фиксим или создаём тикеты.\n\nЭто нужно по двум причинам: во-первых, эти комментарии часто висят\nгодами и с ними ничего не происходит, а во-вторых, они создают\nвпечатление поломанности проекта.\n\nЗадачам по доработке и рефакторингу место в трекере задач, а не в коде.\n\n### Ссылки в инлайновых комментариях\n\nИнлайновый коммент не должен состоять из одной только ссылки.\nЛучше добавлять рядом пояснение. Это позволяет понять суть без того, чтобы\nходить по ссылке.\n\nПлохо:\n\n```python\n# https://stackoverflow.com/q/184618/3694363\n```\n\nХорошо:\n\n```python\n# empties the cache properly: https://stackoverflow.com/q/184618/3694363\n```\n\n## Логирование\n\n### Язык\nВсе логи пишем на английском языке. Это связано с тем, что логи пишутся не\nтолько нашим кодом, но и сторонними библиотеками, которые мы используем - как\nправило, это логи на английском языке. При анализе логов удобно, когда все на\nодном языке - это прибавляет консистентности.\n\nПлохо:\n\n```python\nlogger.info('Запрашиваю баланс')\n```\n\nХорошо:\n\n```python\nlogger.info('Requesting balance')\n```\n\n### Structlog\n\nДля логирования мы используем [structlog](https://www.structlog.org/en/stable/).\nОн умеет создавать логи в JSON, быстро работает и имеет гибкие настройки.\nДля проектов на Django - [django-structlog](https://django-structlog.readthedocs.io/en/latest/).\n\n### Использование контекста\nДля доступа к переменным контекста вместо threadlocal контекста мы используем\nвстроенный [contextvars](https://www.structlog.org/en/stable/contextvars.html).\n\nВот несколько причин, почему contextvars лучше, чем threadlocal:\n\n- Потокобезопасность: contextvars гарантирует потокобезопасность,\nтогда как threadlocal не обеспечивает этого.\n- Читаемость кода: контекстные переменные contextvars легки для чтения и\nпонимания, в отличие от threadlocal, который может вызвать путаницу в коде.\n- Поддержка возможностей асинхронности: contextvars позволяет корректно\nработать с асинхронными вызовами, в то время как threadlocal может отстрелить\nноги\n\nВ целом, использование contextvars вместо threadlocal может повысить\nбезопасность, упростить код и обеспечить правильную работу с асинхронными\nоперациями.\n\n## Строковые литералы и форматирование\n\nИспользуем одинарные кавычки для строковых литералов, но избегаем\nобратных слешей в строках.\n\nДля форматирования используем `.format` или эфстринги с нумерованными или\nименованными аргументами. Не складываем строки с помощью `+`.\n\nЭфстринги используем по желанию и не напихиваем внутри много логики.\n\nПлохо:\n\n```python\n'%s %s' % (first_name, last_name)\n```\n\nПлохо:\n\n```python\n'{} {}'.format(first_name, last_name)\n```\n\nХорошо:\n\n```python\n'{0} {1}'.format(first_name, last_name)\n```\n\n## Импорты\n\nИспользуем [isort](https://github.com/PyCQA/isort).\n\nСущности извне стандартной библиотеки импортируем точечно:\nперечисляем нужные функции и классы.\nНе импортируем весь модуль и точно не делаем `import *`.\nЕсли происходит коллизия имён, используем `as`.\n\nМодули из стандартной библиотеки импортируем целиком.\n\nИмпорты делим на три блока, по PEP8.\nСортировка внутри блока не регламентирована.\n\nПлохо:\n\n```python\nfrom collections import Counter\n```\n\nПлохо:\n\n```python\nfrom core import models\n```\n\nХорошо:\n\n```python\nimport collections\nfrom core.models import Patient\n```\n\n## Код в `__init__.py`\n\nВ `__init__.py` можно писать только импорты. Больше ничего нельзя.\n\n## Форматирование вызовов\n\nЕсли вызов вместе со всеми аргументами умещается на одну строку,\nникаких переносов не ставим.\n\nЕсли вызов не умещается на одну строку, но при переносе\nпосле открывающей скобки всё умещается на две, делаем так.\n\nЕсли аргументов много, есть вложенные вызовы или вызов не помещается\nна две строки, используем расширенное форматирование.\nПри таком форматировании можно ставить несколько аргументов на одну\nстроку, если они схожи по смыслу. Например, `date_from` и `date_to`\nили `to` и `on_delete`.\n\nПлохо:\n\n```python\nrecieved_date = models.DateField(\n    'Дата получения', auto_now_add=True,\n)  # если аргументы помещаются на одной строке, то скобку стоит оставлять там же\n\nlegal = models.ForeignKey(\n    'bestdoctor.ClinicLegalEntity',\n    models.CASCADE,\n    'registries', 'registry',\n    verbose_name='юридическое лицо клиники',\n    )  # скобка с лишним отступом\n```\n\nХорошо:\n\n```python\nrecieved_date = models.DateField(\n    'Дата получения', auto_now_add=True)\n\nlegal = models.ForeignKey(\n    'bestdoctor.ClinicLegalEntity',\n    models.CASCADE,\n    'registries', 'registry',\n    verbose_name='юридическое лицо клиники',\n)\n```\n\nХорошо:\n\n```python\nlegal = models.ForeignKey(\n    'bestdoctor.ClinicLegalEntity',\n    models.CASCADE,\n    'registries',\n    'registry',\n    verbose_name='юридическое лицо клиники',\n)\n```\n\n## Вызов функций и аргументы\n\nПри вызове функций указываем аргументы, если они не очевидны из названия функции.\nЕсли очевидно, всё равно можно писать названия аргументов.\n\nПлохо:\n\n```python\nserializer.is_valid(True)\n```\n\nХорошо:\n\n```python\nserializer.is_valid(raise_exception=True)\n```\n\n## Запятые\n\nУ всех многострочных перечислений ставим запятую после последнего элемента.\nЭто касается не только списков, но и вызовов, туплов и всего\nостального многострочного с запятыми.\n\nЕсли перечисление однострочное, то запятой после последнего элемента не ставим.\nИсключение – `tuple` с одним элементом.\n\nПлохо (потерялась последняя запятая):\n\n```python\nlist_filter = (\n    'legal', 'is_finished', 'is_paid', 'client_registry__record__report_date'\n)\n```\n\nПлохо:\n\n```python\nAct.objects.create(\n    date=today(),\n    act_type='tech',\n    report_date=report_date,\n    is_finished=False\n)\n\nl = [1,2,3,]  # лишняя запятая в конце однострочного перечисления\n```\n\nХорошо:\n\n```python\nlist_filter = (\n    'legal', 'is_finished', 'is_paid', 'client_registry__record__report_date',\n)\n\nAct.objects.create(\n    date=today(),\n    act_type='tech',\n    report_date=report_date,\n    is_finished=False,\n)\n\nl = [1,2,3]\nt = (1,)  # без этой запятой будет не tuple, а int\n```\n\n## Неиспользуемый код\n\nМы хотим, чтобы в нашем коде было как можно меньше багов.\nКлассный способ избавиться от багов – избавиться от кода.\nНет кода – нет проблем.\n\nПоэтому мы удаляем из кодовой базы весь код, который можем.\nЗакомментированный код, неактуальные фичи,\nкакие-то разовые скрипты – всё под нож.\n\nПотом их можно несложно вытащить т.к. все коммиты привязаны к тикету в таск-менеджере,\nпоэтому если понадобится найти удалённый код, всегда можно отыскать тикет,\nпотом его коммиты, а потом накатить их.\n\nВсё, что не нужно прямо сейчас и не понадобится в течение месяца – под нож.\n\n## Данные в настройках\n\nМы стараемся класть данные туда, где им место, и в последнюю очередь храним их в настройках. Вот пример, в котором\nмы переносим данные в БД с помощью `BooleanField`:\n\nБыло:\n\n```python\n# settings.py\nNOTIFY_IN_SLACK_COMPANY_IDS = [123, 456, 789]\n\n# code\nsend_slack_notification(NOTIFY_IN_SLACK_COMPANY_IDS)\n```\n\nСтало:\n\n```python\n# Теперь это просто компании с галочкой notify_in_slack\nnotify_in_slack_company_ids = Company.objects.filter(notify_in_slack=True).value_list('id', flat=True)\nsend_slack_notification(notify_in_slack_company_ids)\n```\n\n## Правила для полей Django-моделей\n\n1. Порядок указания полей регламентирован в\n  [flake8-class-attributes-order](https://github.com/best-doctor/flake8-class-attributes-order).\n1. Названия полей типа `DateTimeField` заканчиваем на `_at`.\n1. Названия полей типа `DateField` заканчиваем на `_date`.\n1. Для `models.CharField` всегда используем `max_length=255`.\n1. Если у поля есть `choices`, то оформляем его через `django.db.models.TextChoices` или `django.db.models.IntegerChoices`.\n1. У каждой модели сразу делаем `__str__` и проверяем, что оно не генерирует много запросов к БД.\n1. Всем полям типа `ForeignKey` обязательно указываем `related_name`.\n1. Если нужно получить айди модели, используем `.pk`, а не `.id`.\n\n## Правила для Django urls\n\n1. В `urls.py` приложения не используем декораторы. Для Class-based views используем `method_decorator` для\n  cоответствующих типов HTTP запросов, подробнее можно почитать о данном декораторе в\n  [официальной документации Django](https://docs.djangoproject.com/en/2.2/topics/class-based-views/intro/#decorating-the-class).\n\n## Называние очередей\nСуть задумки -- начинать название очереди с имени сервиса, который её вычитывает.\n\nПредставим себе набор сервисов `tinker`, `tailor`, `soldier` [, `spy`].\n\nОчередь, в которой `tinker` хранит свои, внутрисервисные задачи,\nкоторые сам создаёт и выполняет, должна называться `tinker-default`.\nЕсли очередей надо будет иметь несколько, можно называть их по характеру задач, например:\n`tinker-heavy`, `tinker-light-long` и т.д.\n\nЧто касается взаимодействия сервисов, предлагаем шаблон `{consuming service name}-rpc`:\nнапример, очередь, в которую `tinker` будет складывать запросы для `soldier`,\nбудет называться `soldier-rpc`.\nОтметим, что название это будет одинаковым на стороне обоих сервисов.\n"
  },
  {
    "path": "guides/python_test_styleguide.md",
    "content": "# Гайдлайны для тестов\n\n## Для кого\n\nЭти правила подразумевают, что их читает Python-разработчик, а не тестер или фронтендер. Так что отделу QA эти правила\nне подойдут.\n\n## Мотивация\n\nТесты, как и любой код, нужно поддерживать в чистоте. Один из способов улучшить сопровождаемость тестов — ввести список\nправил. Правила упростят ознакомление с существующими тестами и подскажут, как писать новые.\n\nЭти гайдлайны являются _вводными_. Добавлять сразу много строгих правил сложно и непрактично, лучше начать с малого.\n\n## Отношение к другим документам\n\nВо время написания тестов соблюдается\n[Python Styleguide](https://github.com/best-doctor/guides/blob/master/guides/python_styleguide.md).\n\n## Дисклеймер\n\nМы пишем тесты с использованием `pytest` и плагинов для него. Рудименты, вроде `unittest.TestCase`, под запретом.\n\n## Два вида тестов\n\nРазработчикам надо тестировать две вещи:\n\n1. Что фича работает\n1. Что функции и методы, составляющие фичу, работают как надо\n\nА соответственно нужны два вида тестов:\n\n1. Интеграционные тесты, которые тестируют фичу целиком и проверяют успешность интеграции разных компонентов системы\n1. Юнит-тесты - легковесные тесты, проверяющие отдельные методы и функции\n\n## Куда класть тесты\n\nТесты для `mail/signals.py` должны лежать в `mail/test_integration/tests.py` или `mail/tests/test_integration/test_signals.py`.\n\nБолее общее правило: тесты для `<foo>/<bar>.py` находятся в `<foo>/test_<test_type>/tests.py`\nили в `<foo>/tests/test_<test_type>/test_<bar>.py`. Если файл `test_<bar>.py` становится слишком большим,\nразносим его на отдельный модуль – `<foo>/tests/test_<test_type>/test_<bar>/test_<some_bar_feature>.py`\n\n## Как именовать тесты\n\nИменование тестов должно отображать тестируемую сущность и тестируемый кейс, схема выглядит так: `test__<функция>__<уточнение кейса>`.\nЕсли возникли сложности с уточнением кейса, возможно ваш тест стоит разбить на несколько.\n\nПримеры: `test__create_company__without_company_service_period`, `test__create_company__with_active_company_service_period`\n\n## `selenium` vs `django.test.Client`\n\nЧтобы протестировать фичу полностью, мы используем `django.test.Client`.\n\nМы не используем `selenium` по двум причинам:\n\n1. Он тяжёлый\n1. Не существует сценариев, в которых нам требуется его функциональность\n\n## Скорость выполнения\n\nМы пока не обращаем внимания на скорость выполнения, но избегаем слишком долгих тестов.\n\n## `success_case` и `error_case`\n\n`success_case`-тест отвечает на вопрос «как должен вести себя код при корректном инпуте». На каждую фичу и её\nсоставляющую должен приходится `success_case`-тест.\n\nЕсли фича/составляющая должна как-то особо обрабатывать некорректный ввод, под неё пишется `error_case`-тест.\n\n## Параметризация\n\nЧтобы избегать дублирования кода при написании тестов, мы их параметризируем с помощью `pytest.parameterize`.\nБлагодаря этому `success_case`-тестов редко бывает больше одного.\n\nБывает так, что из параметров непонятно, какое поведение тестировалось. Тогда группы параметров можно подписать с\nпомощью [параметра ids](https://docs.pytest.org/en/latest/example/parametrize.html#different-options-for-test-ids).\n\n## `pytest.mark.xfail` vs `pytest.raises`\n\nМы используем `pytest.raises`, чтобы показать, что тест проходит только если поднимается исключение.\n\nМы используем `pytest.mark.xfail` в единственном случае: когда мы написали тесты для новой фичи, а код — нет. Коммит\nс такими тестами сломает билд, если их не пометить `pytest.mark.xfail`.\nПри использовании `xfail` также обязательно указывать параметр `reason`, чтобы облегчить менеджмент помеченных тестов\nи не устраивать бардак из бесполезных тестов.\n\n## `mock`\n\nМы используем фикстуру `mocker` из плагина `pytest-mock` для патчинга походов в сеть и асинхронных задач в легковесных юнит-тестах.\n\nФикстура которая мокает метод и возвращает `mock` объект должна именоваться с префиксом `mock_`.\nСам мокнутый метод должен иметь префикс `mocked_`.\n\n## `factory_boy`\n\nМы используем фабрики из `factory_boy` для генерации тестовых Django ORM сущностей и облегчения сетапа тестов.\n\n## Куда класть фабрики и фикстуры\n\nНовые фабрики добавляются по тем же правилам, что и тесты (см. выше) и регистрируются в корневом файле `conftest.py`,\nчтобы быть доступными в любом тестовом модуле.\n\nФикстуры должны располагаться в `conftest.py` соответствующего уровня. Переиспользуемые фикстуры нужно класть как можно выше.\n\n## Фикстуры и фабрики\n\nЧаще всего мы не делаем фикстуры из фабрик.\n\n**Плохо:**\n\n```python\n@pytest.fixture()\ndef n_patients_factory(patient_factory):\n    def generate_n_patients(patients_num, data_only=False):\n        if data_only:\n            return patient_factory.build_dict_batch(patients_num)\n        return patient_factory.create_batch(patients_num)\n```\n\nТакие вещи лучше реализовывать в виде класс-метода на фабрике. Это позволит избежать дублирования усилий.\n\n**Хорошо:**\n\n```python\n@classmethod\ndef create_batch_with_params(cls, patients_num: int, data_only: bool = False) -> List[Patient]:\n    if data_only:\n        return cls.build_dict_batch(patients_num)\n\n    return cls.create_batch(patients_num)\n```\n\n## Уникальные поля моделей в фабриках\n\n### Поля с choices или related\n\nЕсли у модели есть [UniqueConstraint](https://docs.djangoproject.com/en/3.2/ref/models/constraints/#uniqueconstraint) или поля с `unique=True`, то нужно\nдобавлять в фабрики [django_get_or_create](https://factoryboy.readthedocs.io/en/stable/orms.html#factory.django.DjangoOptions.django_get_or_create)\n\n### Текстовые поля\n\nЕсли у модели есть текстовое поле с `unique=True`, то нужно переопределить генерацию этого поля в фабрике\nи добавить к значению `step.sequence`. Что бы при создании нескольких\nсущностей их значение было гарантированно уникальным.\n\nНапример, при помощи такого кода:\n\n`utils/factories.py`:\n```python\nfrom factory import Faker\nfrom factory.builder import BuildStep\n\nclass UniqueStringMixin(Faker):\n    def evaluate(self, instance: Any, step: BuildStep, extra: Any) -> str:\n        value = super().evaluate(instance, step, extra)\n        return f'{step.sequence} {value}'\n```\n\n`app/tests/factories.py`:\n```python\nfrom utils.factories import UniqueStringMixin\n\nclass CompanyFactory(DjangoModelFactory):\n    class Meta:\n        model = Company\n\n    title = UniqueStringMixin('word')\n```\n\n## Аргументы и ассерты теста\n\nТесты нужно держать в чистоте и не перегружать зависимости. Поэтому стоит следить за количеством аргументов теста.\nХорошая тестовая функция должна иметь не более 5-6 аргументов в сингнатуре.\nВ противном случае стоит подумать над разделением проверок.\n\nТо же самое касается количества ассертов в тесте.\nБольшое количество ассертов перегружает тест, снижает его читаемость и полезность.\n\nПри инстанцировании фабрик указываем только те аргументы, которые нужны для текущего теста.\nНе указываем ненужные и не полагаемся на дефолты в фабрике, если оно нужно в конкретном тесте.\n\n## DRY и KISS\n\nТесты, как и любой код, должны придерживаться стайлгайдов и лучших практик.\nДля этого мы выносим переиспользуемые вещи в фабрики и фикстуры, используем хелперы.\n\n## Как тестировать интеграции со сторонними сервисами\n\nМы используем интеграционные тесты для проверки подобных сценариев.\n\nДопустим, мы тестируем функцию, присылающую уведомления в Slack — `send_slack_notification`. Тест будет написан так,\nчтобы сообщение реально приходило на тестовый канал в Slack.\n\n## Как тестировать функциональность, основанную на фичафлагах\n\n Для фичафлагов мы используем библиотеку [waffle](https://github.com/django-waffle/django-waffle). Библиотека\n поставляется с декораторами, которые устанавливают флаг до прогона логики теста. Почитать об их использовании\n можно в [документации](https://waffle.readthedocs.io/en/stable/testing/automated.html).\n\n При добавлении новой фичи, закрытой за фичафлагом, пишется функциональный/юнит тест на эту логику с нужным декоратором\n с параметром `active=True`. Если на старую логику тест существует, то он оборачивается в декоратор с параметром\n `active=False`. Этот подход позволяет быстро приводить в порядок тесты при выпиливании протестированных фичафлагов.\n\n## Решения известных проблем\n\n### Разогрев кеша ContentType\n\nИз-за агрессивного кеширования Django запросов к `ContentType` количество запросов может меняться в зависимости от\nспособа запуска тестов. Это может отразиться на тестах использующих `django_assert_num_queries`.\n\nВ качестве решения при запуске тестов мы принудительно разогреваем кеш:\n\n```python\n@pytest.fixture(scope='session')\ndef django_db_setup(django_db_setup, django_db_blocker):  # noqa: PT004\n    with django_db_blocker.unblock():\n        # Кешируем ContentType для django_assert_num_queries\n        content_type_ids = ContentType.objects.values_list('id', flat=True)\n        for ct_id in content_type_ids:\n            ContentType.objects.get_for_id(ct_id)\n```\n\n## Шаблон теста\n\nМы придерживаемся паттерна ААА в структуре тестов.\n\nСоблюдать эту структуру важно, так как это увеличивает читаемость, а следовательно увеличивает полезность теста.\nОднако, если тест сводится к проверке вызова одной функции и не подразумевает большого сетапа, стоит оставить его\nоднострочником. Например:\n\n```python\ndef test_format_phone_number():\n    assert format_phone_number('88005553535') == '+7(800)-555-35-35'\n```\n\n[Статья про паттерн AAA](https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80)\n\nПример:\n\n```python\n@pytest.mark.parametrize(\n    ('param1', 'param2', '...'),\n    [\n        (value11, value12, ...),\n        (value21, value22, ...),\n        (value31, value32, ...),\n        (..., ..., ...),\n    ]\n)\n@pytest.mark.django_db\ndef test_foo_bar_success_case(param1, param2, ..., foo_factory, ...):\n    arrange()\n\n    act()\n\n    assert ...\n```\n\n## Как дорабатывать документ\n\nЭто не окончательная версия документа. Дорабатывать его можно в репозитории\n[на Гитхабе](https://github.com/best-doctor/guides). Идеи — в ишьюс, реализация — в пулл-реквесты.\n\n"
  },
  {
    "path": "guides/team_guide.md",
    "content": "# Процессы разработки в BestDoctor\n\n## Спринт\n\nСпринт - это процесс выполнения задач, заранее обсужденных и оцененных командой в течении определенного отрезка.\nВ разработке BestDoctor спринт идет 5 дней.\n\n## Время на написание кода и встречи\n\nМы оцениваем задачи и составляем планы на основании того, что разработчик занимается написанием кода в среднем 4\nчаса в день из 8. Остальные 4 часа отводятся на встречи, общение с коллегами и прочее.\n\n## Daily встречи\n\nКаждый день все команды разработки устраивают 10-15 минутную daily встречу, на которой синхронизируются по поводу\nтекущего состояния задач:\n\n* рассказывают все ли идет по плану\n* если что-то идет не по плану, рассказывают почему так и как можно минимизировать потери\n* объясняют какие подводные камни встретились по ходу решения задач, и как их можно побороть.\n  А также что нужно для этого.\n\nЭто нужно для того, чтобы члены команды были синхронизированы, а менеджмент понимал насколько по плану идет\nсобранный спринт.\n\nОпаздывать на такие встречи (как и на все, в общем то) нельзя. Если член команды не может присутствовать оффлайн,\nон уведомляет об этом заранее и его подключают онлайн.\n\nВ экстренных случаях, когда *совсем* не получается быть на встрече онлайн или оффлайн, член команды уведомляет об\nэтом заранее, а потом пишет в чатик команды текстовую версию отчета.\n\n## Грумминг\n\nПререквизитом для планирования спринта является грумминг.\n\nГрумминг - это обсуждение задач с бизнесовой точки зрения. Команда разработки обсуждает что нужно в той или иной\nпостановке задачи, какая от этого польза бизнесу и пользователем.\n\nЗдесь почти или совсем не обсуждаются технические аспекты - этому место на техническом обсуждении.\n\nВ идеале процесс должен быть настроен так, чтобы на момент текущего груминга обсуждались задачи не на ближайший\nспринт, а на спринт, следующий за ним. Таким образом, у команды всегда будет бэклог отгрумленных задач, готовых\nк техническому обсуждению, с минимум недельным запасом.\n\n## Техническое обсуждение и планирование\n\nСпринт начинается с технического обсуждения заранее отгрумленных задач. На этом этапе команда уже имеет представление\nо бизнесовой пользе и необходимости той или иной задачи.\n\nЦель данной встречи  в том, чтобы командно обсудить и утвердить техническое решение задачи, а также определить\nоценку выполнения. Минимальная оценка выполнения задачи - 1 час.\n\n## Ретроспектива спринта\n\nКаждый спринт оканчивается ретроспективой. Это встреча, на которой команда обсуждает проблемы, возникшие в\nтечение спринта, предлагает и обсуждает возможные пути решения, и назначает ответственного за решение.\nПроблемы собираются на протяжении спринта в таблицу вида:\n\n![Retra](https://raw.githubusercontent.com/best-doctor/guides/master/guides/imgs/retra.png)\n\nПустая таблица ретроспективы в конце спринта редко означает, что спринт прошел идеально - скорее это показатель\nтого, что заполнение идет не слишком тщательно. Проблемы не фиксируются, процессы не улучшаются. Это плохо.\n\n## One to One встреча\n\nКаждую неделю участники команды разработки проводят One to One встречи со своим тимлидом. Цель данной\nвстречи - обменяться фидбеком о работе:\n\n* разработчика со стороны тимлида\n* о работе команды со стороны разработчика\n\nГотовиться к этой встрече следует как тимлиду, так и разработчику. Для этого у каждого разработчика есть приватная\nстраница в Notion, доступная только ему и тому, кто проводит One to One.\nЗапись туда должна осуществляться как в течение рабочего флоу (на подобие ретроспективы туда пишется то, о\nчем болит лично у разработчика), так и во время обсуждения проблем - с пометками о возможном решении и\nактуальности предыдущих проблем.\n\n## Cинки\n\nДля синхронизации людей из разных команд, работающих в одном направлении (бэкенд, фронтенд, qa) есть специальные\nвстречи-синки. На них участники направления разработки обсуждают инфраструктурные проблемы, составляют возможные\nрешения, оформляют это в задачи, которые являются инжинирингом.\n\n## Инжиниринг\n\nОдин из пяти спринтовых дней мы занимаемся инженерными задачами. Это разбор технического долга, улучшение\nинфраструктуры, починка технических проблем, с которыми мы столкнулись в течение спринта и прочее. Бэклог\nинжиниринга формируется на основании записей в специальную таблицу вида:\n\n![Engineering](https://raw.githubusercontent.com/best-doctor/guides/master/guides/imgs/engineering.png)\n\nТакже задачи формируются ответственным за направление инжиниринга (фронтенд, бэкенд, qa). На основе этого\nсоставляется инжиниринговый спринт.\n\n## Митапы\n\nBestDoctor проводит внутренние и внешние IT митапы, где разработчики зачитывают доклады по технической тематике\nв образовательных целях. На внутренних митапах выступают только разработчики компании, доклады записываются на\nвидео и доступны на закрытом канале YouTube, с доступом по корпоративной почте. Для внешних приглашаются\nсторонние спикеры. Записи докладов доступны на YouTube в открытом доступе.\n"
  },
  {
    "path": "requirements.txt",
    "content": "rozental-as-a-service==1.1.2\n"
  },
  {
    "path": "setup.cfg",
    "content": "[opensource_watchman]\ntype = readings\nmain_languages = md\nfeatures_from_contributors_are_welcome = False\nlanguage = ru\n"
  }
]