🎓 Практический курс по Backend разработке: artemshumeiko.ru 💡 Попробуй онлайн-тренажёр для подготовки к техническому собеседованию: clck.ru/3B5gwP 💡 Забирай роадмап изучения самого востребованного фреймворка на Python - FastAPI здесь: t.me/ArtemShumeikoBot
Спасибо за видео! Артём, несколько моментов: 1. В методе TasksService.__init__ в качестве tasks_repo мы ожидаем класс, поэтому аннотируем как type[AbstractRepository], а не AbstractRepository. 2. Имеет смысл сохранять разные версии API в учебных (в этом случае) целях, чтобы можно было сравнить разницу архитектур. 3. Может только мне, но при просмотре видео белая тема жёстко выжигает глаза, особенно ночью. Думаю, лучше использовать тёмную.
Познакомился с FastAPI 3 месяца назад. Начав работать сразу обратил внимание, что надо бы как то это все вынести подальше, в результате вышло, что то подобное на луковую архитектуру. Теперь знаю, как это правильно и читабельно упаковать, спасибо! Хотелось бы пример с фильтрацией, который можно так же переиспользовать!
22:51 указана неправильная типизация входного параметра tasks_repo, должно быть Type[AbstractRepository]. Тогда и подсказки для self.tasks_repo корректно будут работать без дублирования типа
А что с join делать? В смысле мы в методах получаем просто id для FK, а хорошо бы полную модельку получить с подтянутыми полями. Если в наследнике репозитория переопределить метод и добавить join, то смысл базового репозитория теряется
Артем, спасибо за видео! Лучший русскоязычный канал по FastAPI. Подскажите, а в каком слое правильнее обрабатывать исключения связанные с БД? Вроде бы логично в SQLALchemyRepository, ведь мы будем ловить конкретные ошибки SQLAlchemy, но, с другой стороны, мы ведь можем обрабатывать исключения в соответствии с какой-то бизнес-логикой, что кажется правильно держать в сервисном слое?
Зависит от того, какие исключения. Если NotFound404 то я, например, бросаю это исключение из репозитория, но с возможностью управлять этим repository.get_one(raise_exception=True). При таком подходе можно заказать получение объекта либо None либо исключение в зависимости от логики. Все остальные исключения можно либо обрабатывать в сервисном слое, либо общим обработчиком fastapi, возвращая 500 server error и логируя ошибку куда-нибудь.
Очень хороший и полезный урок, сам недавно на Java такое реализовывал. Но многие говорят, что это антипатерн и, действительно, сейчас работаю в месте, где проект на Flask и большинство запросов к БД реализованы внутри эндпоинта. Если для каждой из таблиц реализовывать CRUD репозиторий и сервис, то код МКСа вырастет для гигантских размеров
Ну так добро пожаловать на энтерпрайз уровень. Обычно когда много таблиц, большая часть CRUDа создаётся копипастой. А детали реализации вам так и так писать, разница лишь в том, где этот код лежать будет, только в одном случае у вас везде стандартный crud, а в другом "Вась, не помнишь где мы это написали?" И это, почему то люди предпочитают джипы и кроссоверы, а не малолитражки, хотя последние и проще в производстве.
Что если нужно добавить некую логику присущую только для конкретного репозитория? Например в случае с тасками "удалить каждую вторую таску") Логично будет создавать на репозитории метод TaskRepository.delete_every_second_tasks. Однако дёргать из TaskService такой метод будет неверно (self.tasks_repo.delete_every_second_tasks) ведь абстракция AbstractRepository не имеет такого метода.
мне кажется ты в TasksService будешь принимать не абстрактный репозиторий, а TasksRepository просто. Ну и в TasksRepository ты можешь наследоватся и просто добавить нужные детали, или переопределить нужный метод.
А подскажи можно ли сделать такую же архитектуру как в django с разделением на отдельные приложения или как аналог blueprint во flask, есть у fastapi какое то название для таких приложений и используют ли такой подход? Просто везде, где видел как пишут fast api, обычно всё роуты всех приложений кладут в одну папку routes, по другому с fast api разделение делают
Каким образом лучше организовать код, если нужно написать специфичный sql запрос, который нужен только для определенного сервиса? Писать функцию внутри сервиса, либо создавать новый репозиторий?
Спасибо за видео! Что ты лумаешь по поводу того, чтобы использовать Протоколы вместо абстрактных базовых классов? Протоколы позволяют задать имена и типы атрибутов класса/экземпляра, параметров методов, а также типы возвращаемых методами значений. Это не получится сделать с помощью ABC.
Привет! Хмм а когда ты делаешь в find_all методе res = [row[0].to_read_model это не нужно никак обложить уровнем абстракции так же? Т.е такой кейс, ты меняешь алхимию на что то другое, откуда ты знаешь что у тебя модельки соответствуют колу to_read_model внутри реализации SQLNotAlchemyRepository?
А почему в абстрактном классе в методах нет self, а просто пустые скобки? А что будет если добавить? Мне просто pycharm красным подчёркивает, но в ошибку не выпадает
Подскажи, а нельзя в контроллере использовать что-то вроде: task_service: TaskServece = Depends(TaskService)? А в самом таск сервисе таким же образом получать репо?
Скажите, а тайпхинт AbstractRepository в ините сервиса точно правильный? Судя по тому, что в ините tasks_repo вызывается (как конструктор), его тип не AbstractRepository, а класс этого репозитория (не знаю как правильно указать это в хинте)
А какой смысл каждый раз при вызове TaskService передавать в него TaskRepository? Разве не логичнее в конструкторе TaskService объявить что-нибудь а-ля self.repo = TaskRepository()?
Буква D из принципов SOLID - Dependency Inversion. В данном случае это значит, что TaskService не должен зависеть от конкретной реализации TaskRepository
Насчёт указания типа "абстрактный репо" - тоже такая себе идея. У тебя что, все репозитории одинаковы будут? Смысл тогда в них? Что будешь делать при специфичных задачах?
А какую альтернативу можешь предложить? Вместо указания абстрактного типа репозитория указывать конкретную реализацию? Думаю это может быть нарушением принципа D в SOLID, может есть ещё какие-то варианты или вообще иной подход к построению архитектуры?
На случай, если забудешь реализовать какой-нибудь метод у класса, который наследуется от абстрактного. Тогда при вызове не реализованного метода возникнет эта ошибка
Должно стоять расширение pylance и дальше при наведении на неимпортированную библиотеку появится всплывающее окошко, в котором можно будет импортировать, либо после наведения нажать CTRL + .
Какой-то Java EE. Нет никаких проблем писать sql в эндпоинтах. Он как раз гораздо понятнее и нагляднее там сидит. В крайнем случае для длинных запросов можно вынести выражение в отдельный модуль. Когда у вас с фронта просят ручки со вложенными сущностями вам нужно делать joinedload/selectinload. Где-то вам нужен User+Tasks, где-то только User. Вы на каждый такой вариант будете делать по функции в репозитории? Или передавать опции отдельным параметром, ещё больше всё усложняя? Модели и схемы можно очень быстро посмотреть одним кликом из эндпоинта, а вот прыгать по ORM надстройкам кастомным - это как раз разбираться нужно. Ну и по поводу притянутой за уши задачи уровня "смены sqlalchemy" - такое не происходит почти никогда. Да и на что? Вы скорее на другой язык переписывать будете.
Никто не может запретить писать плохо, каждый человек или команда пишет как ему удобно. Можно вообще все в одном файле написать, так просто сверх наглядно будет, я считаю. 😂 А уж как навариться на поддержке потом можно будет, долгими месяцами пытаясь найти баг - просто мечта. 😊 При многослойной архитектуре вы не модель меняете в ручках, а вызов функции сервиса, или сервис правите не трогая ручку вообще. Работу со вложенными сущностями да и вообще при объединении данных нескольких таблиц вам так и так где то писать надо вне зависимости от архитектуры и ее наличия в принципе 😂 , можно конечно навалить все в эндпоинты, компьютер все стерпит.
лучше объявлять сессию 1 раз уровнем выше. Часто объявляют сессию на уровне эндпоинта/ручки и прокидывают через Depends дальше в те модули, которым нужна сессия для работы с базой данных.
Ахах спасибо Артем, хорошо было бы если бы ты в фишки pidantic посвятил)) и магия , классов настройки , валидации + у нас ещё используется dependancy_injector хорошо было бы осветить его. P.S. шарлотку поставил :DDD
Это максимальный тариф с собеседованием, консультацией, файлом для подготовки к собеседованию, постоянной поддержкой в телеграмме, созвонами группы и прочим. Есть и другие тарифы с меньшим набором услуг - каждый выбирает под себя :)
Несколько вещей сделаны ужасно: почему в sqlalchemy модели метод для преобразования данный в пидентик? По твоим словам - что будешь делать, когда захочешь поменять orm? Также вопросы что будешь делать, когда некоторые связные модели подгружать не нужно будет Далее про инициализацию репозитория в инициализаторе сервиса + открытие сессии ради каждого метода репозитория - вообще орнул. Тебе не больно, когда ради 2-3 вызовов репозитория в одном сервисе ты будешь открывать на каждый новую сессию алхимии? Пропихнул бы в своем DI сессию ОДНУ и работала бы во время обработки запроса - так экономишь ресурсы
engine используется только для одной цели - передачи в фабрику async_sessionmaker. Фабрика в дальнейшем используется для получения сессий, причем значение переменной, хранящей фабрику, нигде не изменяется. Зачем уходить от использования глобальных переменных в данном случае?
@@andreyarefev445 ты точно понимаешь во что вытекают глобальные переменные? 1. Ты не контролируешь жизненный цикл переменной, например в твоем случае engine (не можешь очистить пул коннектов, выполнив dispose). 2. Весь твой код, буквально весь код, где используется глобальная переменная становится зависим от 1-ой казалось бы переменной, что вытекает в сложную поддержку и тестирование (сложно написать юнит тест, сложно подменять объект, сложно подменять его конфигурацию). 3. Ты привел в пример множества фабрик на проекте, но ты же понимаешь, если ты захочешь в какой-то момент в репозитории иметь подключение к другой БД, следовательно ты должен будешь завести новый engine и async_sessionmaker и подменить их там где нужно, то тебе надо будет менять весь код, который использует предыдущие глобалы. Вот и тебе переиспользование кода, о котором говорится в видео)
🎓 Практический курс по Backend разработке: artemshumeiko.ru
💡 Попробуй онлайн-тренажёр для подготовки к техническому собеседованию: clck.ru/3B5gwP 💡
Забирай роадмап изучения самого востребованного фреймворка на Python - FastAPI здесь: t.me/ArtemShumeikoBot
Хочу большой курс по sqlalchemy и alembic. Лайк стоит)
Хорошо когда есть кто-то, кто за тебя запишет видео по нужной теме)))))
Спасибо за видео! Артём, несколько моментов:
1. В методе TasksService.__init__ в качестве tasks_repo мы ожидаем класс, поэтому аннотируем как type[AbstractRepository], а не AbstractRepository.
2. Имеет смысл сохранять разные версии API в учебных (в этом случае) целях, чтобы можно было сравнить разницу архитектур.
3. Может только мне, но при просмотре видео белая тема жёстко выжигает глаза, особенно ночью. Думаю, лучше использовать тёмную.
не канал, а находка 👍
Понятно, спокойно, можно и за обедом послушать😊
Познакомился с FastAPI 3 месяца назад. Начав работать сразу обратил внимание, что надо бы как то это все вынести подальше, в результате вышло, что то подобное на луковую архитектуру. Теперь знаю, как это правильно и читабельно упаковать, спасибо!
Хотелось бы пример с фильтрацией, который можно так же переиспользовать!
Спасибо за ролик, обязательно продолжай)
Превосходно 💯 даже несмотря на опыт коммерческой разработки на fastapi хочется поддерживать и смотреть с твоего канала для новичков, спасибо !!!
весьма интересно, однозначно лайк!
Спасибо, информативно! по большому счету данную практику можно применить и к другим языкам.
Срасибо за видео. Полностью согласен с данным подходом. Всё просто пончтно, и так нужно писать проекты.
Спасибо за полезное видео!)
А чем отличается onion architecture от clean architecture?
Там же еще есть mvc, mvp, mvt и возможно что то еще
22:51 указана неправильная типизация входного параметра tasks_repo, должно быть Type[AbstractRepository]. Тогда и подсказки для self.tasks_repo корректно будут работать без дублирования типа
Наконец-то я понял для чего нужны абстрактные классы
А где видео с unit of work?
Артем, можешь пожалуйста записать видео с различиями между pydantic 1.* и 2.0?
А что с join делать? В смысле мы в методах получаем просто id для FK, а хорошо бы полную модельку получить с подтянутыми полями. Если в наследнике репозитория переопределить метод и добавить join, то смысл базового репозитория теряется
ДАВАЙ КУРС ПО АЛХИМИИ И АЛЕМБИКУ, ПО ЧАСУ КАЖДЫЙ ДЕНЬ!
Артем, спасибо за видео! Лучший русскоязычный канал по FastAPI.
Подскажите, а в каком слое правильнее обрабатывать исключения связанные с БД? Вроде бы логично в SQLALchemyRepository, ведь мы будем ловить конкретные ошибки SQLAlchemy, но, с другой стороны, мы ведь можем обрабатывать исключения в соответствии с какой-то бизнес-логикой, что кажется правильно держать в сервисном слое?
В FastApi есть HTTPException. Через него и выводишь ошибку пользователю в формате json
Зависит от того, какие исключения. Если NotFound404 то я, например, бросаю это исключение из репозитория, но с возможностью управлять этим repository.get_one(raise_exception=True). При таком подходе можно заказать получение объекта либо None либо исключение в зависимости от логики. Все остальные исключения можно либо обрабатывать в сервисном слое, либо общим обработчиком fastapi, возвращая 500 server error и логируя ошибку куда-нибудь.
Очень хороший и полезный урок, сам недавно на Java такое реализовывал. Но многие говорят, что это антипатерн и, действительно, сейчас работаю в месте, где проект на Flask и большинство запросов к БД реализованы внутри эндпоинта. Если для каждой из таблиц реализовывать CRUD репозиторий и сервис, то код МКСа вырастет для гигантских размеров
Ну так добро пожаловать на энтерпрайз уровень. Обычно когда много таблиц, большая часть CRUDа создаётся копипастой. А детали реализации вам так и так писать, разница лишь в том, где этот код лежать будет, только в одном случае у вас везде стандартный crud, а в другом "Вась, не помнишь где мы это написали?" И это, почему то люди предпочитают джипы и кроссоверы, а не малолитражки, хотя последние и проще в производстве.
Что если нужно добавить некую логику присущую только для конкретного репозитория? Например в случае с тасками "удалить каждую вторую таску") Логично будет создавать на репозитории метод TaskRepository.delete_every_second_tasks. Однако дёргать из TaskService такой метод будет неверно (self.tasks_repo.delete_every_second_tasks) ведь абстракция AbstractRepository не имеет такого метода.
мне кажется ты в TasksService будешь принимать не абстрактный репозиторий, а TasksRepository просто. Ну и в TasksRepository ты можешь наследоватся и просто добавить нужные детали, или переопределить нужный метод.
Так, а ты решил этот вопрос для себя? Можешь поделиться, если да?
А подскажи можно ли сделать такую же архитектуру как в django с разделением на отдельные приложения или как аналог blueprint во flask, есть у fastapi какое то название для таких приложений и используют ли такой подход? Просто везде, где видел как пишут fast api, обычно всё роуты всех приложений кладут в одну папку routes, по другому с fast api разделение делают
Я разделил всю логику по отдельным папкам, но как разделить миграции, чтобы они брались из папок с приложениями
Каким образом лучше организовать код, если нужно написать специфичный sql запрос, который нужен только для определенного сервиса? Писать функцию внутри сервиса, либо создавать новый репозиторий?
Создавать репу и в ней писать запрос. Затем обращаться к репе внутри сервиса
Спасибо за видео! Что ты лумаешь по поводу того, чтобы использовать Протоколы вместо абстрактных базовых классов? Протоколы позволяют задать имена и типы атрибутов класса/экземпляра, параметров методов, а также типы возвращаемых методами значений. Это не получится сделать с помощью ABC.
Привет! Хмм а когда ты делаешь в find_all методе res = [row[0].to_read_model это не нужно никак обложить уровнем абстракции так же?
Т.е такой кейс, ты меняешь алхимию на что то другое, откуда ты знаешь что у тебя модельки соответствуют колу to_read_model внутри реализации SQLNotAlchemyRepository?
Да, конвертацию модели бд в доменную модель приложения лучше вынести в отдельную сущность-класс
А почему в абстрактном классе в методах нет self, а просто пустые скобки?
А что будет если добавить? Мне просто pycharm красным подчёркивает, но в ошибку не выпадает
круто! спасибо!
А почему скрыли основной курс по FaspAPI - 18 недоступных видео скрыто. Ранее было доступно. Верните пожалуйста.
Приходите на Практический курс по Backend разработке с 0 до production-ready кода: artemshumeiko.ru
Подскажите как импортировать класс через контекстное меню? Как называется такой плагин?
Вроде как встроен в Visual Studio code
Я через CTRL + . вызываю его или правой кнопкой мыши
Возможно кстати это pylance
@@artemshumeiko спасибо за ответ. У меня одно из расширений vscode мешало работе. Удалил и все заработало!
Подскажи, а нельзя в контроллере использовать что-то вроде:
task_service: TaskServece = Depends(TaskService)?
А в самом таск сервисе таким же образом получать репо?
Можно, ищи Dependency Injector python
Спасибо за видео! Репозитории же по сути и есть DAO, или я что то путаю?
да, почти одно и то же
1. Переопределяешь сигнатуры дочерних классов, что не ок.
2. Не показал, как работаешь с селектом и перегоняешь модели между слоями
А что насчет типизации? Можно ли получать из базы не абы что, а типизированный обьект/экземпляр класса?
Для этого нужны дженерики
Скажите, а тайпхинт AbstractRepository в ините сервиса точно правильный? Судя по тому, что в ините tasks_repo вызывается (как конструктор), его тип не AbstractRepository, а класс этого репозитория (не знаю как правильно указать это в хинте)
посмотрел, вроде так и указывается, Type[AbstractRepository]
А какой смысл каждый раз при вызове TaskService передавать в него TaskRepository? Разве не логичнее в конструкторе TaskService объявить что-нибудь а-ля self.repo = TaskRepository()?
Буква D из принципов SOLID - Dependency Inversion. В данном случае это значит, что TaskService не должен зависеть от конкретной реализации TaskRepository
Насчёт указания типа "абстрактный репо" - тоже такая себе идея. У тебя что, все репозитории одинаковы будут? Смысл тогда в них? Что будешь делать при специфичных задачах?
А какую альтернативу можешь предложить? Вместо указания абстрактного типа репозитория указывать конкретную реализацию? Думаю это может быть нарушением принципа D в SOLID, может есть ещё какие-то варианты или вообще иной подход к построению архитектуры?
Хочу большой курс по алхимии и алембик =)
Зачем в абстрактных методах рейзить `NotImplementedError`?
это нужно на время разработки, чтобы не забыть
На случай, если забудешь реализовать какой-нибудь метод у класса, который наследуется от абстрактного. Тогда при вызове не реализованного метода возникнет эта ошибка
@@codEnjoyer советую тогда попробовать не реализовать абстрактный метод у класса наследуемого от ABC и попробовать создать экземпляр)
Очень удобно получилось.
Никто не поймет, пока не объяснишь.
Люблю такие архитектуры
Фабрика еще нужна что бы выбирать конкретную реализацию репозитория
Подскажите, кто в курсе, как он импортирует пакеты, чтобы они автоматически вставали вверх на свои места? Че то не попадался мне такой
Должно стоять расширение pylance и дальше при наведении на неимпортированную библиотеку появится всплывающее окошко, в котором можно будет импортировать, либо после наведения нажать CTRL + .
@@artemshumeiko ок спасибо, ато неудобно либо вспоминать либо копировать с других файлов приходилось
Какой-то Java EE. Нет никаких проблем писать sql в эндпоинтах. Он как раз гораздо понятнее и нагляднее там сидит. В крайнем случае для длинных запросов можно вынести выражение в отдельный модуль.
Когда у вас с фронта просят ручки со вложенными сущностями вам нужно делать joinedload/selectinload. Где-то вам нужен User+Tasks, где-то только User. Вы на каждый такой вариант будете делать по функции в репозитории? Или передавать опции отдельным параметром, ещё больше всё усложняя?
Модели и схемы можно очень быстро посмотреть одним кликом из эндпоинта, а вот прыгать по ORM надстройкам кастомным - это как раз разбираться нужно.
Ну и по поводу притянутой за уши задачи уровня "смены sqlalchemy" - такое не происходит почти никогда. Да и на что? Вы скорее на другой язык переписывать будете.
Никто не может запретить писать плохо, каждый человек или команда пишет как ему удобно. Можно вообще все в одном файле написать, так просто сверх наглядно будет, я считаю. 😂 А уж как навариться на поддержке потом можно будет, долгими месяцами пытаясь найти баг - просто мечта. 😊
При многослойной архитектуре вы не модель меняете в ручках, а вызов функции сервиса, или сервис правите не трогая ручку вообще. Работу со вложенными сущностями да и вообще при объединении данных нескольких таблиц вам так и так где то писать надо вне зависимости от архитектуры и ее наличия в принципе 😂 , можно конечно навалить все в эндпоинты, компьютер все стерпит.
а чем она отличается от чистой архитектура Дяди Боба
может про него тоже видос
Здравствуйте, будет ли показан подход CBV?
В этом видео нет. В будущих - возможно
а это нормально что мы каждый раз будем писать with async_session_maker() ? в каждом методе
лучше объявлять сессию 1 раз уровнем выше. Часто объявляют сессию на уровне эндпоинта/ручки и прокидывают через Depends дальше в те модули, которым нужна сессия для работы с базой данных.
Ахах спасибо Артем, хорошо было бы если бы ты в фишки pidantic посвятил)) и магия , классов настройки , валидации + у нас ещё используется dependancy_injector хорошо было бы осветить его. P.S. шарлотку поставил :DDD
Репозиторий не должен управлять транзакцией
class Config - тоже устаревший синтаксис. Новый : model_config = ConfigDict(**kwargs)
Спасибо. Во втором видео про архитектуру поправлю это
20к за курс? Оо
Это максимальный тариф с собеседованием, консультацией, файлом для подготовки к собеседованию, постоянной поддержкой в телеграмме, созвонами группы и прочим. Есть и другие тарифы с меньшим набором услуг - каждый выбирает под себя :)
уже 26к, скоро будет 400 000 минимум
Несколько вещей сделаны ужасно:
почему в sqlalchemy модели метод для преобразования данный в пидентик? По твоим словам - что будешь делать, когда захочешь поменять orm? Также вопросы что будешь делать, когда некоторые связные модели подгружать не нужно будет
Далее про инициализацию репозитория в инициализаторе сервиса + открытие сессии ради каждого метода репозитория - вообще орнул. Тебе не больно, когда ради 2-3 вызовов репозитория в одном сервисе ты будешь открывать на каждый новую сессию алхимии? Пропихнул бы в своем DI сессию ОДНУ и работала бы во время обработки запроса - так экономишь ресурсы
нужно избавляться от глобальных переменных, в частности от глобального engine и async_session_maker.
зачем?
engine используется только для одной цели - передачи в фабрику async_sessionmaker. Фабрика в дальнейшем используется для получения сессий, причем значение переменной, хранящей фабрику, нигде не изменяется. Зачем уходить от использования глобальных переменных в данном случае?
Зачем нам множество фабрик сессий в проекте? Зачем тогда нужен будет async_session_maker если он не будет глобальным?
@@andreyarefev445 ты точно понимаешь во что вытекают глобальные переменные? 1. Ты не контролируешь жизненный цикл переменной, например в твоем случае engine (не можешь очистить пул коннектов, выполнив dispose). 2. Весь твой код, буквально весь код, где используется глобальная переменная становится зависим от 1-ой казалось бы переменной, что вытекает в сложную поддержку и тестирование (сложно написать юнит тест, сложно подменять объект, сложно подменять его конфигурацию). 3. Ты привел в пример множества фабрик на проекте, но ты же понимаешь, если ты захочешь в какой-то момент в репозитории иметь подключение к другой БД, следовательно ты должен будешь завести новый engine и async_sessionmaker и подменить их там где нужно, то тебе надо будет менять весь код, который использует предыдущие глобалы. Вот и тебе переиспользование кода, о котором говорится в видео)
тишка, ты?
Кажется, не хватает dipendecy injection
*promo sm*
Предлагаю на английском языке вести чтоб пацаны развивались
Зачем? Есть много курсов и материалов на английском.
на англ. весь интернет забит материалами. А на русском как раз мало. Особенно когда надо понять какие-то абстракции.
Неплохо, но не до конца. Нужно добавить дженерик в объявление абстрактного репозитория и возвращать его в методах.