Задать вопрос
  • Ошибка expected expect or finally block.Как решить?

    Frostealth
    @Frostealth
    Backend Developer
    Исключения в python
    try:
        # your code
    except ExceptedException as exc:
        # handle the exception
    Ответ написан
    Комментировать
  • Вопрос по путям в yii2?

    Frostealth
    @Frostealth
    Backend Developer
    Лучше, конечно, обойтись без путей в БД.

    Если все же требуется, то реализовать вполне возможно.
    Достаточно реализовать свой `UrlRule` (дока).
    пример из доки

    namespace app\components;
    
    use yii\web\UrlRuleInterface;
    use yii\base\BaseObject;
    
    class CarUrlRule extends BaseObject implements UrlRuleInterface
    {
    
        public function createUrl($manager, $route, $params)
        {
            if ($route === 'car/index') {
                if (isset($params['manufacturer'], $params['model'])) {
                    return $params['manufacturer'] . '/' . $params['model'];
                } elseif (isset($params['manufacturer'])) {
                    return $params['manufacturer'];
                }
            }
            return false;  // данное правило не применимо
        }
    
        public function parseRequest($manager, $request)
        {
            $pathInfo = $request->getPathInfo();
            if (preg_match('%^(\w+)(/(\w+))?$%', $pathInfo, $matches)) {
                // Ищем совпадения $matches[1] и $matches[3] 
                // с данными manufacturer и model в базе данных
                // Если нашли, устанавливаем $params['manufacturer'] и/или $params['model']
                // и возвращаем ['car/index', $params]
            }
            return false;  // данное правило не применимо
        }
    }

    Ответ написан
    1 комментарий
  • Как не задавать елемент в массиве, если его значение null?

    Frostealth
    @Frostealth
    Backend Developer
    array_filter
    $utms = [
        'utm_source' => $_POST['utm_source'] ?? null,
        'utm_medium' => $_POST['utm_medium'] ?? null,
        'utm_campaign' => $_POST['utm_campaign'] ?? null,
        'utm_term' => $_POST['utm_term'] ?? null,
        'utm_content' => $_POST['utm_content'] ?? null,
    ];
    
    // отфильтрует значения по empty
    $utms = array_filter($utms);
    
    // отфильтрует только null
    $utms = array_filter($utms, fn($val) => $vall !== null)
    Ответ написан
    1 комментарий
  • Did you forget to call 'init_app', or did you create multiple 'SQLAlchemy' instances?

    Frostealth
    @Frostealth
    Backend Developer
    В приведенном вами примере на github (https://github.com/alonewasser/z) убираем хендлер сигнала @app.before_request и делаем следующее:

    db = SQLAlchemy(app)
    with app.app_context():
        from .models import *
        db.create_all()
        create_default_admin()


    P.S. Рекомендую инициалицировать БД и добавлять администратора консольной (cli) командой. Перед первым запуском веб-приложения запустили нужную команду и забыли. И приложение не будет лезть лишний раз в БД при обработке каждого запроса.
    пример cli

    Объявление команд.
    @app.cli.command('init_db')
    def init_db():
        from .models import *
        db.create_all()
    
    @app.cli.command('init_admin')
    def init_admin():
        from .models import User
        db.session.add(User(username='admin', ...))
        db.session.commit()


    Документация: https://flask.palletsprojects.com/en/2.3.x/cli/#cu...
    Пример запуска команд в консоли
    >>> flask --app project.app init_db
    >>> flask --app project.app init_admin

    Ответ написан
  • Как решить данную ошибку с inline кнопками?

    Frostealth
    @Frostealth
    Backend Developer
    1. Смотрим текст ошибки
    pydantic.error_wrappers.ValidationError: 1 validation error for InlineKeyboardMarkup
    inline_keyboard
      field required (type=value_error.missing)

    2. Смотрим внимательно доку
    3. Profit

    спойлер

    class aiogram.types.inline_keyboard_markup.InlineKeyboardMarkup(*,
        inline_keyboard: List[List[InlineKeyboardButton]],  // required
        **extra_data: Any
    )

    Ответ написан
  • Как в laravel использовать 2 гуарда для одного маршрута?

    Frostealth
    @Frostealth
    Backend Developer
    Указать нужные guards при указании middleware в параметрах.
    Route::middleware('auth:api,web')->get('/some_page', 'SomeController@api');
    Ответ написан
    1 комментарий
  • Почему когда подписываюсь на Doctrine.Events::preUpdate, он выполняется бесконечно?

    Frostealth
    @Frostealth
    Backend Developer
    Вызов flush() в процессе обработки preUpdate приводит к бесконечной рекурсии, ибо обработчик события вызывается в процессе выполнения предыдущего вызова flush().
    Цепочка вложенных вызовов в вашем случае выглядит следующим образом:

    - EntityManager::flush()
    - UnitOfWork::commit()
    - UnitOfWork::executeUpdates()
    - ListenersInvoker::invoke()
    - OrderEventSubscriber::preUpdate() - вызывается еще до обновления Order и его удаления из UnitOfWork
    - $this->notificationsCreator->createChangeStatusNotification() - создает рекурсию вызовом `flush()`
    - EntityManager::flush() - снова пытается обновить данные Order и вызывает обработку событий `preUpdate`
    - ...
    - EntityManager::flush()
    - ...

    Вызов методов persist(), remove() и т.п. в процессе обработки событий доктрины может привести к неожиданным результатам. Данные просто могут быть не сохранены, как минимум. Крайне не рекомендую менять какие-либо данные в Entity, EM/UoW при обработке событий доктрины.

    В вашем случае можно реализовать свое событие, например OrderStatusChanged. Но лучше использовать более конкретные события вроде OrderCompleted и OrderCanceled.
    И воспользоваться symfony/event-dispatcher.
    Либо создавать уведомление непосредственно там, где выполняете изменение статуса заказа (контроллер, сервис и т.п., зависит от вашей архитектуры) и вызывается flush().
    пример верного вызова

    $this->em->flush();
    $this->events->dispatch(new OrderCompleted($order->id));
    
    // или
    $this->em->flush();
    $this->notificationCreator->createChangeStatusNotification($order->getCustomer());



    P.S. Рекомендую использовать Guard Clauses для уменьшения вложенности и улучшения чтения кода.
    пример

    public function preUpdate(PreUpdateEventArgs $args)
    {
        $entity = $args->getObject();
        if (!$entity instanceof Order) {
        	return;
        }
    
        $this->logger->info($entity);
        $onlyStatusChanged = count($args->getEntityChangeSet()) === 1 && $args->hasChangedField('status');
        if ($onlyStatusChanged) {
            $this->notificationsCreator->createChangeStatusNotification($entity->getCustomer());
        }
        // много кода...
    }

    Ответ написан
    1 комментарий
  • Как исправить при миграции ошибку - SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Temporary failure:?

    Frostealth
    @Frostealth
    Backend Developer
    Контейнер у вас именован как db, а в `.env` прописали mysql.
    Попробуйте заменить хост в `.env` DB_HOST=db.

    Также стоит убедиться, что у контейнера с приложением явно прописана зависимость от db:
    depends_on:
      - "db"


    И очистить кэш Laravel php artisan config:clear
    Ответ написан
  • Как исправить ошибку отображения контента шаблона Laravel blade на удаленном VPS сервере?

    Frostealth
    @Frostealth
    Backend Developer
    Может из-за использования @else для @auth?
    https://laravel.com/docs/10.x/blade#authentication...
    @auth 
        // ...
    @endauth
    
    @guest 
        // ...
    @endguest
    Ответ написан
    Комментировать
  • Как отправлять данные в сеть не имея IP адреса?

    Frostealth
    @Frostealth
    Backend Developer
    Для начала можно почитать что из себя представляет DHCP и OSI/ISO. И самое главное - найти в интеренете материалы по компьютерным сетям и изучить их.
    Вкратце, DHCP - это выдача IP адреса какому-либо узлу сети, а IP - это сетевой протокол, соответствующий L3 модели OSI.

    Для чего вам от него отказываться - не понятно. Если нужно работать на канальном уровне (L2), то следует изучить технологию Ethernet или PPP.
    Ответ написан
    Комментировать
  • Почему выводит ошибку(sqlite3)?

    Frostealth
    @Frostealth
    Backend Developer
    Потому что происходит попытка добавить пользователя с `user_id`, который уже существует.
    В таблице уже есть пользователь с таким `user_id`, и БД выдает ошибку об этом, т.к. стоит ограничение о том, что `user_id` должен быть уникальным, без дублей.

    Добавьте AUTOINCREMENT для `user_id` в таблице и не указывайте `user_id` при добавлении нового пользователя. В таком случае БД будет сама выставлять числовой идентификатор для новых юзеров.
    Другой вариант - это использовать uuid4 или ему подобные для генерации случайного идентификатора на стороне приложения.

    Предлагаю почитать: https://metanit.com/sql/sqlite/2.3.php

    В случае, если данные юзеров, в т.ч. и идентификаторы, приходят из одного внешнего источника (например из телеграма) и нужно опираться на них, то необходимо проверять наличие юзера в таблице по `user_id`. Если такого юзера нет - добавлять, если уже есть - обновлять.

    Также можно поступить проще: INSERT ON CONFLICT DO.
    -- если пользователь уже существует, то просто игнорировать
    INSERT INTO 'users' (user_id) VALUES (?) ON CONFLICT(user_id) DO NOTHING
    
    -- если пользователь уже существует, то обновить ему `referrer_id`
    INSERT INTO 'users' (user_id, referrer_id) VALUES (?, ?) 
        ON CONFLICT(user_id) DO UPDATE SET referrer_id = excluded.referrer_id
    Ответ написан
  • Где найти отправку письма с восстановлением пароля в дефолтной авторизации laravel?

    Frostealth
    @Frostealth
    Backend Developer
    PasswordBroker вызывает метод `sendPasswordResetNotification` у экземплара юзера.
    В модели юзера, вероятно, используется трейт CanResetPassword, который и реализует указанный метод, отправляя уведомление ResetPassword.

    Иными словами, отправка письма находится в методе `sendPasswordResetNotification()` экземпляра класса `app\Models\User`, а реализация метода определена трейтом `\Illuminate\Auth\Passwords\CanResetPassword`.
    Ответ написан
    Комментировать
  • Как максимально быстро добавить огромное количество записей в БД без дублей?

    Frostealth
    @Frostealth
    Backend Developer
    Как вариант: отфильтровать `$numbers` на стороне PHP и вставить пачкой.

    // избавляемся от возможных дубликатов
    $numbers = collect($numbers)->unique();
    // pluck('num') вернет нам список значений атрибута num, а не список моделей
    $existingNumbers = Model::query()->whereIn('num', $numbers->toArray())->pluck('num');
    // с помощью diff получаем элементы, которых нет в $existingNumbers
    $newNumbers = $existingNumbers->diff($numbers)->mapWithKeys(function ($value, $key) {
        // ['one', 'two'] => [['num' => 'one'], ['num' => 'two']]
        return [$key => ['num' => $value]];
    });
    
    Model::query()->getConnection()->transaction(function () use ($newNumbers) {
        Model::query()->insert($newNumbers->toArray()); 
    });


    При очень больших данных в БД нужно вытаскивать из БД чанками (пачками определенного размера, например по 500 000), а не сразу все.
    // избавляемся от возможных дубликатов
    $newNumbers = collect($numbers)->unique();
    Model::query()->toBase()->whereIn('num', $numbers->toArray())
          ->chunk(500000, function ($existingNumbers) use (&$newNumbers) {
               // с помощью diff получаем элементы, которых нет в $existingNumbers
              $newNumbers = $newNumbers->diff($existingNumbers);
          });
    
    // ['one', 'two'] => [['num' => 'one'], ['num' => 'two']]
    $newNumbers = $newNumbers->mapWithKeys(function ($value, $key) {
        return [$key => ['num' => $value]];
    });
    
    Model::query()->getConnection()->transaction(function () use ($newNumbers) {
        Model::query()->insert($newNumbers->toArray()); 
    });


    Также можно воспользоваться ON CONFLICT, если СУБД поддерживает подобное. Например, у SQLite - ON CONFLICT DO, у MySQL - INSERT IGNORE. Это позволит избавиться от выгрузки данных из БД, что уменьшит потребление памяти приложением и сократит количество запросов.
    Laravel имеет для этого метод Query::insertOrIgnore(), который будет глушить все ошибки от некоторых БД, но для некоторых не поддерживается. Поддерживаемые БД: MySQL, SQLite, PostreSQL.
    Необходимо, чтобы на атрибут `num` в БД стоял constraint unique, иначе БД просто вставит дубликат.
    $numbers = collect($numbers)->unique()->mapWithKeys(function ($value, $key) {
        return [$key => ['num' => $value]];
    });
    
    Model::query()->getConnection()->transaction(function () use ($numbers) {
        Model::query()->insertOrIgnore($numbers->toArray());
    });


    Индексы и explain изучить не помешает. Размер чанка подобрать по возможностям железа.
    Индекс на num значительно ускорит выборку, но скорость вставки снизится.
    Так же отказ от ORM (Eloquent), объектов и использование голого SQL с PDO ускорит работу.
    На большие данные и нагрузки нужно мощное железо. Может потребоваться масштабирование и т.д.
    Ответ написан
    Комментировать
  • Почему свойства экземпляра класса не являются изолироваными в данном примере?

    Frostealth
    @Frostealth
    Backend Developer
    Потому что `A::__data` ссылается на общий для всех экземпляров список. Предлагаю почитать.

    Ошибка заключается в присвоении значения списка (работает так со всем) свойству класса. Другими словами, свойство класса будет содержать ссылку на объявленный список, которая будет и у экземпляров класса.
    class A:
        __data = []  // список будет общий для всех экземпляров класса A


    Присвоить значение непосредственно экземпляру класса можно в методе `__init__`
    class A:
        def __init__(self):
            self.__data = []
    Ответ написан
    5 комментариев
  • Как объединить две локальные сети через OpenVPN?

    Frostealth
    @Frostealth
    Backend Developer
    Не помешают конфиги OpenVPN и таблицы маршрутизации с обоих роутеров.
    Предполагаю, что на роутере1 (кв1) отсутствует маршрут до сети кв2 через VPN.
    Исходный ответ

    Настройка маршрутов в ASUS описана здесь: https://www.asus.com/ru/support/FAQ/1011706/

    Итак, требуется на роутере1 прописать маршрут к роутеру2 для сети кв2.
    Маршрут примерно такой:
    - IP-адрес сети: 192.168.1.0
    - Сетевая маска: 255.255.255.0
    - Шлюз: IP-адрес роутера2 внутри сети VPN, например 10.8.0.2

    И не забыть прописать для роутера2 статический адрес (10.8.0.2) для OpenVPN. Иначе OpenVPN сервер может выдать роутеру2 другой IP.

    Без маршрута, роутер1 не будет понимать на какой порт/интерфейс направлять запросы к сети 192.168.1.0/24.

    Если не заработает, то, вероятно, через сеть VPN в сеть клиента VPN отсутствует NAT. Тогда следует сконфигурировать брандмауэр (firewall) на роутерах, в т.ч. и на роутере1. Настройка брандмауэра в ASUS описана здесь: https://www.asus.com/ru/support/FAQ/1013630/

    Необходимо, чтобы брандмауэр на роутере1 пропускал запросы из сети 192.168.1.0/24 в сеть 192.268.0.0/24. И наоборот, на роутере2 - из сети 192.168.0.0/24 в сеть 192.168.1.0/24. Без этого роутер будет отбрасывать все запросы во внутреннюю сеть из других сетей.


    При решении данного вопроса с автором возникли проблемы с роутерами ASUS. Клиент (роутер2) ни в какую не видел сеть за сервером (возможно упустил какую-то опцию, "route ..", "iroute" и "push 'route ...'" не помогли). Решение с tap не подходило из-за отключения DHCP на роутере2.
    Основная проблема - это невозможность загрузить в роутер целиком файл конфигурации сервером. А документацию консоли роутера (через ssh) найти быстро не удалось.
    Если кто знает решение, прошу описать его в комментариях.

    Аренда VPS (выделенного сервера) с Ubuntu и поднятие OpenVPN сервера уже на нем с последующим подключением двух роутеров в качестве клиента решили проблему.
    Можно поднять и на ПК в сети, конечно. В таком случае нужно будет добавить статический IP для этого ПК и выполнить проброс портов в настройках роутера.

    Установку и основную настройку сервера OpenVPN помог упростить скрипт https://git.io/vpn, который пришлось редактировать для замены зашитой в нем сети 10.8.0.0 на другую в связи с наличием пересечения данной сети с другой на одном из роутеров.

    Итак, установка и конфигурация сервера OpenVPN на Ubuntu для объединения двух сетей роутеров без выхода в интернет через VPN. Для начала необходимо подключиться к серверу по SSH.

    Установка OpenVPN с помощью скрипта через терминал. Скачиваем скрипт и запускаем его командами ниже.
    >>> wget https://git.io/vpn -O openvpn-install.sh
    >>> sudo bash openvpn-install.sh


    Отвечаем на все запросы скрипта, выбираем протокол UDP.
    После завершения установки открываем файл nano /etc/openvpn/server/server.conf, удаляем все строки, начинающиеся на push, и добавляем следующее:
    ifconfig-pool-persist ipp.txt
    client-config-dir /etc/openvpn/server/ccd
    client-to-client
    
    route 192.168.0.0 255.255.255.0
    push 'route 192.168.0.0 255.255.255.0'
    
    route 192.168.1.0 255.255.255.0
    push 'route 192.168.1.0 255.255.255.0'

    Пояснение к конфигу

    ifconfig-pool-persist ipp.txt - будет сохранять соответствия выданных им IP адресов в файл `ipp.txt`. Таким образом, каждый клиент будет всегда получать один и тот же IP.

    client-config-dir /etc/openvpn/server/ccd - указывает серверу директорию с дополнительной конфигурацией для каждого клиента.

    client-to-client - разрешает передачу данных между клиентами.

    route 192.168.0.0 255.255.255.0 - уведомляет сервер о локальной сети (192.168.0.0/24) клиента.

    push 'route 192.168.0.0 255.255.255.0' - сервер будет передавать маршрут к указанной сети всем клиентам.


    Далее создадим указанную в конфиге директорию sudo mkdir /etc/openvpn/server/ccd.
    И добавим в нее файлы, названия которых будут отражать имена наших клиентов (указываются далее). И пропишем в них какая сеть какому клиенту принадлежит.
    /etc/openvpn/server/ccd/router1

    iroute 192.168.0.0 255.255.255.0

    /etc/openvpn/server/ccd/router2

    iroute 192.168.1.0 255.255.255.0


    Запускаем скрипт снова для добавления клиента. Для добавления второго запускаем потом еще раз.
    После запуска скрипта вводим цифру 1, соответствующую опции 1) Add a new client.
    Здесь же и указываем название клиента (router1, router2).
    >>> sudo bash openvpn-install.sh

    Скачиваем файлы конфигурации для клиентов, которые сгенерировал скрипт, и загружаем соответствующий конфиг при создании OpenVPN клиента на роутерах.
    Маршруты на роутерах прописывать не надо, как и правила в брандмауэре. OpenVPN сервер сам отправляет маршруты клиентам, которые мы указали в его конфиге ранее с помощью push 'route ...'.
    Ответ написан
    1 комментарий
  • Как сделать wildCard route на nestJs?

    Frostealth
    @Frostealth
    Backend Developer
    Вам стоит сначала изучить документацию фреймворка.

    WildCard - это другое

    WildCard - это не про параметры роута, а про шаблонность. Иными словами, когда `a/b/c`, `a/c/c`, `a/d/c` и т.д. являются одним роутом `a/*/c`.
    https://docs.nestjs.com/controllers#route-wildcards


    Для получения параметра роута используйте декоратор `@Param()`
    @Get('findOne/:username')
    async getUserByUsername(
        @Param('username') username: string,
    ): Promise<UserEntity['id'] | undefined> {
        return await this.usersService.findOne(username);
    }


    Примеры есть в доке: https://docs.nestjs.com/controllers#route-parameters
    Ответ написан
    Комментировать
  • Чем отличаются dto, entity...?

    Frostealth
    @Frostealth
    Backend Developer
    DTO (Data Transfer Object, объект передачи данных) - это объект, единственной целью которого является передача каких-либо данных между слоями, компонентами системы и т.д. По сути это просто структура. Никакой бизнес-логики DTO не содержит. Только данные в своих полях. Обычно DTO являются иммутабельными (immutable - неизменяемый), что предполагает невозможность изменить данные внутри DTO после его создания.

    Пример DTO

    // с использованием read-only properties
    class CustomerDTO {
        public final integer id;
        public final string name;
    
        public CustomerDTO(integer id, string name) {
            this.id = id;
            this.name = name;
       }
    }
    
    // с использованием геттеров (getters)
    class CustomerDTO {
        private integer id;
        private string name;
    
        public CustomerDTO(integer id, string name) {
            this.id = id;
            this.name = name;
        }
    
        public integer getId() {
            return this.id;
        }
    
        public string getName() {
            return this.name;
        }
    }



    Entity - это сущность. Сущности представляют объекты предметной области (модели), описывают свойства и поведение. Они отличаются продолжительным циклом жизни и индивидуальностью. Простыми словами, они имеют идентификатор (id) и их где-то хранят между запросами, например в БД.
    Идентификатор (id) сущности позволяет нам отличать одного Васю Пупкина от другого Васи Пупкина. И тот, и другой - Вася Пупкин, но их важно различать по какому-нибудь уникальному признаку.

    Пример Entity

    class Customer {
        private integer id;
        private string firstName;
        private string lastName;
        private integer walletBalance;  // кошелек здесь ради примера
    
        public Customer(integer id, string firstName, string lastName) {
            this.id = id;
            this.firstName = firstName;
            this.lastName = lastName;
            this.walletBalance = 0;
        }
    
        public integer getId() {
            return this.id;
        }
    
        public string getFirstName() {
             return this.firstName;
        }
    
        public string getLastName() {
            return this.lastName;
        }
    
        public string getFullName() {
            return this.firstName + " " + this.lastName;
        }
    
        public void rename(string firstName, string lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
    
        public void takeMoney(Money money) {
            this.walletBalance += money.amount;
        }
    
        public Money giveMoney(integer amount) throws InsufficientFunds {
            if (this.walletBalance < amount) {
                throw new InsufficientFunds();
            }
    
            this.walletBalance -= amount;
            return Money(amount);
        }
    }



    есть dto которая указывает как валидировать данные от клиента

    DTO, по идее, не должна содержать никаких правил валидации. Но для упрощения и удобства правила все же прописывают прямо в DTO, обычно с помощью аннотаций (декораторов, аттрибутов - зависит от языка).

    у нас есть entity которая описывает то как данные хранятся в бд

    Аналогично с DTO. В угоду простоты и удобства жертвуют независимостью, детали хранения сущностей протекают в сами сущности.

    как называется то что позволяет обрезать данные отправляемые на клиент

    Не совсем понятно что вы имеете в виду. Примеры могли бы помочь понять.
    Но, вероятно, речь идет о сериализации (Serialization). Сериализатор (Serializer) трансформирует объекты, например DTO, в другую структуру данных, например в JSON.
    Пример использования сериализации

    serializer.serialize(new CustomerDTO(1, "John Smith")); // -> {"id": 1, "name": "John Smith"}

    Ответ написан
    Комментировать
  • Как удалить пустой элемент многомерного массива?

    Frostealth
    @Frostealth
    Backend Developer
    array_filter может принимать callback функцию и флаг ARRAY_FILTER_USE_BOTH, благодаря которому в callback функцию будет передан ключ элемента массива вторым аргументом

    $array = [
        [
            "" => "что то не нужное",
            "brand" => "Бренд",
            "code" => "Артикул",
            "price" => "цена",
            "quantity" => "кол-во",
            "name" => "нейм",
        ],
    ];
    
    $filter = function ($value, $key) {
        return !empty($key);
    };
    
    $result = array_map(function ($value) use ($filter) {
        return array_filter($value, $filter, ARRAY_FILTER_USE_BOTH);
    }, $array);
    Ответ написан
    Комментировать
  • Как сделать экспорт сразу двух gridview в yii2-export?

    Frostealth
    @Frostealth
    Backend Developer
    Насколько я понял, данное расширение не умеет работать сразу с двумя таблицами (GridView).

    Если у вас таблицы идентичны, то что мешает выводить одну общую? Если разные, то и экспорт будет для каждой свой.

    Пример страницы с двумя таблицами (GridView) с экспортом средствами данного расширения: yii2-export Multiple Export Menus
    Ответ написан
    Комментировать
  • Как автоматически запускать консольную команду (Yii2)?

    Frostealth
    @Frostealth
    Backend Developer
    Попробуйте supervisor
    Ответ написан
    Комментировать