Код на салфетке
2.23K subscribers
748 photos
15 videos
2 files
791 links
Канал для тех, кому интересно программирование на Python и не только.

Сайт: https://pressanybutton.ru/
Чат: https://t.iss.one/+Li2vbxfWo0Q4ZDk6
Заметки автора: @writeanynotes

Реклама и взаимопиар: @Murzyev1995
Сотрудничество и др.: @proDreams
Download Telegram
Прошёл ещё месяц учебы, вернее не учёбы, а её завершения.

Настало время "отчётного" поста на Plkabu.

https://pikabu.ru/story/obuchenie_vosemnadtsatyiy_mesyats_posledniy_11058755?utm_source=linkshare&utm_medium=sharing

Расчехляйте лайкомёты 😊
🔥7
Django 40. Собственные страницы ошибок
Автор: Иван Ашихмин

Ошибки на сайте случаются постоянно, но далеко не все из них происходят по вине программиста. Самая знаменитая ошибка – 404 Not Found – относится к пользовательским ошибкам, когда был совершён переход на несуществующую страницу. Ещё одна известная ошибка – 500 Internal Server Error – относится к ошибкам на стороне сервера. Это означает, что сервер не справился с какой-то задачей.

Всё это называется "Кодом состояния HTTP" или по простому "HTTP статус кодом".

Всего их пять видов:
1. 1xx - информационные.
2. 2xx - успешные.
3. 3xx - перенаправления.
4. 4xx - ошибки клиента.
5. 5xx - ошибки сервера.

Полный список кодов с описанием доступен в википедии: https://ru.wikipedia.org/wiki/Список_кодов_состояния_HTTP

Нас интересуют 4xx и 5xx статус коды, проще говоря – ошибки.
🔥4
Мы с вами сделаем страницы отображающиеся при следующих ошибках:
- 400 Bad Request – когда был отправлен неправильный запрос или данные.
- 403 Forbidden – когда пользователь пытается получить доступ к страницам сайта, на просмотр которых у него нет разрешения.
- 404 Not Found – когда пользователь переходит на несуществующую страницу.
- 500 Internal Server Error – когда на стороне сервера что-то пошло не так и поднялась необработанная ошибка.


Утилитарное приложение utils_app.
Для обработки ошибок нам необходимо написать обработчики. Дабы не писать их в основных приложениях, создадим новое utils_app.
Для этого выполним команду: python manage.py startapp utils_app.

Не забудьте сразу добавить новое приложение в INSTALLED_APPS


Обработчики ошибок.
Нам необходимо написать четыре простейшие функции, которые будут вызываться при возникновении определённой ошибки.

В директории приложения создадим файл exception_handlers.py.

Создадим четыре функции:
- bad_request_handler - для обработки 400-й ошибки.
- permission_denied_handler - для обработки 403-й ошибки.
- page_not_found_handler - для обработки 404-й ошибки.
- server_error_handler - для обработки 500-й ошибки.

Каждая функция принимает два аргумента request и exception. Обратите внимание, что аргумент exception не всегда передаётся в обработчик, по этому устанавливаем ему значение по умолчанию None.

В каждой функции возвращаем работу функции render, передавая в аргументах request, путь до файла шаблона и статус-код ошибки.


Код функций:
from django.shortcuts import render  
from rest_framework import status


def bad_request_handler(request, exception=None):
return render(request, "utils_app/400.html", status=status.HTTP_400_BAD_REQUEST)


def permission_denied_handler(request, exception=None):
return render(request, "utils_app/403.html", status=status.HTTP_403_FORBIDDEN)


def page_not_found_handler(request, exception=None):
return render(request, "utils_app/404.html", status=status.HTTP_404_NOT_FOUND)


def server_error_handler(request, exception=None):
return render(request, "utils_app/500.html", status=status.HTTP_500_INTERNAL_SERVER_ERROR)



Шаблон страницы с ошибкой.
Нам нужно четыре шаблона. Создадим в директории с шаблонами новую директорию для приложения utils_app и в ней файлы шаблонов.

У меня четыре шаблона. Все четыре одинаковые, за исключением текста, поэтому покажу пример только одного шаблона:
{% extends "blog/base.html" %}
{% block title %}404 - страница не найдена!{% endblock %}

{% block content %}
<div class="d-flex align-items-center justify-content-center vh-100">
<div class="text-center">
<h1 class="display-1 fw-bold">404</h1>
<p class="fs-3"><span class="text-danger">Ой!</span> Страница не найдена.</p>
<p class="lead">
Запрашиваемая вами страница не найдена.
</p>
<a href="{% url 'blog:index' %}" class="btn btn-primary my-btn">Вернуться на главную</a>
</div>
</div>
{% endblock %}


Выводим текст соответствующий статус-коду.


Подключение обработчиков.
Осталось подключить обработчики. Для этого откроем главный файл urls.py, расположенный в директории проекта рядом с файлом settings.py.
🔥5
В нём в конце файла добавляем четыре переменные. Значением переменных будет строка вида "название_приложения.файл_с_обработчиками.название_функции_обработчика".

handler400 = "utils_app.exception_handlers.bad_request_handler"  
handler403 = "utils_app.exception_handlers.permission_denied_handler"
handler404 = "utils_app.exception_handlers.page_not_found_handler"
handler500 = "utils_app.exception_handlers.server_error_handler"


Обратите внимание! Чтобы собственные страницы ошибок заработали, параметр DEBUG в файле settings.py должен быть установлен в False!


Заключение.
Вот таким вот простым и нехитрым способом можно сделать собственные страницы ошибок в Django.

Файлы к посту, можно получить в боте по коду: 102630

Пост на сайте
Поддержать проект

#Python #Django #гайды #Bad_Request #страницы_ошибок #обработчики_ошибок #Forbidden #Internal_Server_Error #Not_Found
🔥8👍1
Что выведет код с изображения ниже?
Anonymous Quiz
16%
3
8%
2
45%
[0, 1, 2]
31%
TypeError
👍4🤯3
Что выведет этот код? №7
Вчерашняя задача - это яркий пример полиморфизма в ООП. Суть в том, что мы создаём "класс-потомок" и изменяем его поведение относительно наследуемого класса, но обо всём по порядку.

В викторине приняли участие 31 человек из них верно ответили всего 5. Большая часть сошлись во мнении, что должна выйти ошибка TypeError. Вторым популярным ответом стал [0, 1, 2], означающий, что должен вывестись весь список.


Код задачи:
class MyList(list):
def __init__(self, *args):
super().__init__()
for i in args:
self.append(i)

def __hash__(self):
return 1

def __str__(self):
return str(self.__len__())


lst = MyList(0, 1)
lst.append(2)
dct = {lst: lst}
print(dct[lst])



Разберём задачу.
Начнём с класса. Мы создали класс MyList и унаследовали его от встроенного в Python класса list. Этот класс, экземпляры хорошо известной нам структуры данных - списка.

В нашем вновь созданном классе мы переопределяем три метода:
- Конструктор класса __init__ – в этом методе инициализируем super-класс, т.е. вызываем метод __init__ из родительского класса. Далее в цикле добавляем все переданные позиционные аргументы в список, используя метод append, который мы унаследовали из родительского класса привычного нам списка.
- dunder-метод __hash__ – данный метод возвращает хэш-значение объекта класса. Если тип данных не хэшируется (как, например, типа данных список), то возвращается None. В нашем случае мы возвращаем значение 1, что соответствует True.
- dunder-метод __str__ – данный метод возвращает строковое представление объекта класса. Для списков это - "[1, 2, 3]", в нашем случае возвращается строка с указанием длинны списка (за это отвечает dunder-метод __len__).

После класса идёт ряд действий:
1. В переменную lst помещается объект созданного нами только что класса MyList с аргументами (0, 1). Тем самым формируется список [0, 1] (здесь и дальше мы будем употреблять слово "список" условно, называя так объекты нашего авторского класса, унаследованного от списка).
2. К списку добавляется число 2, получается следующий список - [0, 1, 2].
3. В переменную dct помещаем словарь, в котором ключ и значение - это значение, записанное в переменную lst (то есть объект нашего авторского класса со списком [0, 1, 2]).
4. Последним шагом выводим значение словаря передав объект из переменной lst как ключ.

Почему удалось добавить список как ключ?
Тут всё достаточно просто. Действительно, словарь не даёт делать ключом нехэшируемые объекты, и на то есть свои причины: списки и другие изменяемые типы данных не хэшируются, поскольку изменение приводило бы к изменению их хэша, а словари хранят ключи в хэш-таблице. Таким образом, записав значение по одному ключу, который в последствии изменится, мы не сможем получить записанные данные.

Что касается нашего случая, то когда мы переопределили dunder-метод __hash__, мы изменили поведение класса list, сделав его хэшируемым. Именно это позволило использовать переменную со списком в качестве ключа.

Почему в выводе 3?
И тут ответ "на поверхности". Когда для вывода объектов используется функция print, вызывается dunder-метод __str__. Мы переопределили его, возвращая вместо стандартного строкового представления списка – его длину.

Заключение.
Вот такая задачка у нас вышла. В задаче показан пример полиморфизма в действии, когда класс list изменяет своё поведение, будучи унаследован пользовательским классом.
🔥9🤯1
AIOgram3 16. Перевод голосовых сообщений в текст
Автор: Иван Ашихмин

Голосовые сообщения – весьма спорная тема. Кто-то без них жить не может, а кто-то ненавидит их всей душой. Если опустить прения на эту тему, то у голосовых сообщений есть одна проблема: их банально не всегда удобно слушать. Чаще всего голосовые сообщения – это "информация в моменте", и, когда появляется возможность прослушать их, они теряют свою актуальность.

У нас в чате любят слать голосовые, и, чтобы все могли оставаться "в теме", даже когда не у каждого есть возможность прослушать их, мы решили научить бота переводить голосовые в текст.

Нейронных сетей, заточенных под распознавание голоса, свыше десятка, но они требуют серьёзных вычислительных мощностей, чего мой не самый мощный сервер не может предоставить.
🔥5😁1😱1
Готовые сервисы тоже есть, но практически все - платные. Не беря в расчёт "пробные периоды" из бесплатных (вернее с бесплатным лимитом в месяц) есть два сервиса:

- Google Cloud - думаю не стоит рассказывать о Google. Они предлагают бесплатный тариф - 60 минут в месяц.
- SpeechFlow - сервис, позиционирующий себя как "лидера рынка". Поддерживают транскрипцию с 14-ти языков. Предлагают бесплатный тариф: 30 минут онлайн-распознавания (на сайте) и пять часов по API в месяц.

Сервис от Google нам не подходит: 60 минут на целый месяц слишком мало, поэтому выбор был сделан в пользу SpeechFlow. Пять часов в месяц – тоже не то чтобы много, но с этим уже можно работать. Ну, и будет интересно посмотреть статистику длительности голосовых и их количества.


Регистрация и получение API-ключа.
Переходим на сайт сервиса: https://speechflow.io/ru/

Регистрация максимально проста, особенно если заходить через Google-аккаунт.

После регистрации попадаем в личный кабинет. Там в левой панели выбираем раздел "API".
На открывшейся странице нажимаем кнопку "Сгенерировать ключ API".
В появившемся окне будет две строки:

- KeyId - идентификатор ключа
- KeySecret - секретный ключ
ВНИМАНИЕ! Ключ отображается всего один раз! Сохраните его в удобном для вас месте.

Также на этой странице есть примеры кода на различных языках, включая и Python. Пример для Python избыточный. Мы возьмём его за основу и упростим для нашей задачи.


Отслеживание и расшифровка голосовых сообщений.
В посте "AIOgram3 14. Фильтруем запрещённые слова" мы писали обработку для проверки сообщений на запрещённые слова. В этом посте мы дополним ее функционалом проверки голосовых сообщений.

Но сперва добавим ключи в класс Secrets.

Откроем файл settings.py и в классе Secrets добавим две строки:
audio_key_id: str = "ваш_KeyId"  
audio_key_secret: str = "ваш_KeySecret"


В значение полей вставьте полученные от SpeechFlow данные доступа.


Обработка голосовых сообщений.
Откройте файл filter_words.py.

Тут у нас уже есть функция check_message с условием if message.text. Для того, чтобы "отлавливать" голосовые сообщения, нужно другое условие - if message.voice.

Добавим его ниже, использовав elif.

Сперва нам необходимо скачать голосовое сообщение с сервера Telegram. Для этого выполним несколько шагов:
1. Создадим переменную file_id, в которую получим идентификатор голосового сообщения.
2. Создадим переменную file, в которую асинхронно получим информацию о файле голосового сообщения.
3. Создадим переменную file_path, в которую получим путь до файла.
4. Создадим переменную file_name, в которой укажем путь и имя сохраняемого файла на нашем сервере. Убедитесь, что директория, в которой вы собираетесь сохранять файл, существует.
5. Асинхронно выполним команду скачивания файла.

Получается вот такой блок кода:
file_id = message.voice.file_id  
file = await bot.get_file(file_id)
file_path = file.file_path
file_name = f"files/audio{file_id}.mp3"
await bot.download_file(file_path, file_name)


Создаём четыре переменные, которые понадобятся далее в коде:
1. headers – в этой переменной создаём словарь с ключами keyId и keySecret, в которые передаём значения соответствующих полей из класса Secrets.
2. create_url – в этой переменной определяем URL-адрес, на который отправляем файл для транскрипции. Обратите внимание на query-параметр lang: в нём указываем язык голосового сообщения.
👍3🔥1😱1
3. query_url – в этой переменной определяем URL-адрес, на который будем обращаться для получения результата транскрипции.
4. files – в этой переменной создаём словарь с ключом file, в значение которого помещаем открытый аудиофайл.

Блок кода:
headers = {"keyId": Secrets.audio_key_id, "keySecret": Secrets.audio_key_secret}  
create_url = "https://api.speechflow.io/asr/file/v1/create?lang=ru"
query_url = "https://api.speechflow.io/asr/file/v1/query?taskId="
files = {"file": open(file_name, "rb")}


Далее отправляем POST-запрос на адрес create_url и записываем ответ в переменную response.
Если статус-код ответа равен статус-коду 200, проваливаемся внутрь условия. Иначе завершаем выполнение обработки.

Внутри условия в переменную create_result получаем JSON ответа.
Затем к переменной query_url добавляем идентификатор задачи из create_result и query-параметр resultType.
Всего есть четыре значения resultType:

1. Тип результата по умолчанию: формат JSON для предложений и слов с указанием начального и конечного времени.
2. Формат JSON для сгенерированных субтитров с указанием начального и конечного времени.
3. Формат SRT для сгенерированных субтитров с указанием начального и конечного времени.
4. Чистый текстовый формат для результатов транскрипции без указания начального и конечного времени.

Для ответа в чате лучше всего подходит 4-й вариант.

Блок кода:
response = requests.post(create_url, headers=headers, files=files)  
if response.status_code 200:
create_result = response.json()
query_url += create_result["taskId"] + "&amp;resultType=4"


Запускаем бесконечный цикл.
Внутри цикла отправляем GET-запрос на адрес query_url и сохраняем ответ в переменную response.
Если статус-код ответа равен 200, проваливаемся внутрь условия. Иначе прерываем цикл и завершаем выполнение обработки.

В переменную query_result получаем JSON ответа.

Далее у нас два условия:

Если в query_result по ключу code лежит значение 11000, значит, транскрипция завершена.
Проваливаемся внутрь условия.

Проверяем, что в query_result по ключу result есть данные, иначе прерываем цикл. Такое может произойти, когда сервису не удалось распознать речь на голосовом или если оно было без речи вовсе.

В переменную result получаем значение ключа result из query_result. Для удобства можно дополнительно применить метод .replace(), чтобы заменить два переноса строки, например, на пробел.
Асинхронно отправляем ответ на голосовое сообщение.
Удаляем файл с сервера.
Прерываем цикл.

Иначе-если (elif) в query_result по ключу code лежит значение 11001, значит транскрипция ещё не завершена и стоит попробовать чуточку позже.
Выставляем сон на три секунды и переходим к следующей итерации цикла.

Если оба условия не были удовлетворены, то мы прерываем цикл.


Полный код:
🔥4😱1
import os
import requests
import time

from botlogic.settings import bot, Secrets


elif message.voice:
file_id = message.voice.file_id
file = await bot.get_file(file_id)
file_path = file.file_path
file_name = f"/code/pressanybutton_bot/files/audio{file_id}.mp3"
await bot.download_file(file_path, file_name)

headers = {"keyId": Secrets.audio_key_id, "keySecret": Secrets.audio_key_secret}
create_url = "https://api.speechflow.io/asr/file/v1/create?lang=ru"
query_url = "https://api.speechflow.io/asr/file/v1/query?taskId="
files = {"file": open(file_name, "rb")}

response = requests.post(create_url, headers=headers, files=files)
if response.status_code 200:
create_result = response.json()
query_url += create_result["taskId"] + "&amp;resultType=4"
while True:
response = requests.get(query_url, headers=headers)

if response.status_code 200:
query_result = response.json()
if query_result["code"] 11000:
if query_result["result"]:
result = query_result["result"].replace("\n\n", " ")
await message.reply(f"&lt;pre&gt;&lt;code&gt;{result}&lt;/code&gt;&lt;/pre&gt;")
os.remove(file_name)
break
elif query_result["code"] 11001:
time.sleep(3)
continue
else:
break
else:
break



Заключение.
Качество распознавания не всегда хорошее. Тот же WhisperAI на модели small выдавал куда лучший результат, но он требователен к железу. Интересно, как быстро кончится лимит в 5 часов.

Файлы к посту, можно получить в боте по коду: 790636

Пост на сайте
Поддержать проект

#Python #Гайды #Telegram #AIOgram #Telegram_бот #AIOgram3 #бот #нейронные_сети #whisper #SpeechFlow #транскрипция #голосовые_сообщения
🔥5👍1😱1
Приветствую вас, друзья, в этот волшебный вечер пятницы!

А волшебен он именно тем, что впереди два дня свободы от трудовых будней, начальства и всяческой рутины. И выходные лучше всего начать с просмотра хорошего фильма.

Фильм: Семь

Год: 1995

Детектив Уильям Сомерсет - ветеран уголовного розыска, мечтающий уйти на пенсию и уехать подальше от города и грешных обитателей. За 7 дней до пенсии на Сомерсета сваливаются две неприятности: молодой напарник Миллс и особо изощренное убийство. Острый ум опытного сыщика сразу определяет, что за этим преступлением, скорее всего, последуют другие. Новости подтверждают его догадку. Поняв, что убийца наказывает свои жертвы за совершенные ими смертные грехи, детектив встает перед выбором: вернуться к работе либо уйти и передать дело своему менее опытному напарнику?

https://www.sspoisk.ru/film/377/?utm_referrer=www.google.com

Хорошего просмотра)
🔥6
Что выведет этот код? №8
🥳Нас уже 404🥳
😁12🎉5🔥4😱2🤪1
Вчера мы задали простую, но весьма хитрую задачку. Суть задачи заключается в понимании типов данных Python, а именно того, как работают изменяемые типы данных. Перед чтением разбора рекомендуем прочитать наш пост "Питон на измене".

В викторине приняли участие 44 человека! Верных ответов, правда, набралось всего 8, что равно 18-ти процентам... Самым популярным вариантом, оказался "Ничего из перечисленного выше", набрав 39%, следом за ним идёт первый вариант с 32% ответов.


Код задачи:
my_list = [[1, 2, 3]] * 3
my_list[0].append(4)
my_list[1].pop()
my_list[2].append(5)
print(my_list)



Разберём задачу.
Создаём переменную my_list в которой умножаем список [[1, 2, 3]] на три. Получаем следующий список: [[1, 2, 3], [1, 2, 3], [1, 2, 3]].

Далее производим ряд действий над элементами списка:
1. В нулевой элемент списка добавляем цифру 4.
2. Из первого элемента списка вынимаем последнюю цифру.
3. Во второй элемент списка добавляем цифру 5.
4. Выводим весь список на экран.

В итоге мы ожидаем получить [[1, 2, 3, 4], [1, 2], [1, 2, 3, 5]].

Почему тогда верный ответ `[[1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5]]`?
Вспоминаем, каким типом данных является список? Верно, изменяемым. Ещё такие типы данных именуют "ссылочными".

Всё дело в том, что когда мы в самом начале умножали список [[1, 2, 3]] на три, мы по сути создали список из трех ссылок на один и тот же список.

Это легко проверить, запустив следующий код:
my_list = [[1, 2, 3]] * 3  
print(id(my_list))
print(id(my_list[0]), id(my_list[1]), id(my_list[2]))

>>> 2239539514496
>>> 2239534636480 2239534636480 2239534636480


Как видим, все три элемента списка ссылаются на одну и ту же ячейку памяти, т.е. на один и тот же список.

В связи с этим, изменяя один элемент списка, мы изменяем и остальные.


Заключение.
Вот так вот простейший с виду код может обмануть программиста.
🔥5
This media is not supported in your browser
VIEW IN TELEGRAM
Мы ещё дипломы не получили, а ГБ уже предлагает сменить профессию...

Видео записал одногруппник/подписчик @mssw_wssm
😁7🤡2👀1
AIOgram3 17. Подготовка к разворачиванию на сервере
Автор: Иван Ашихмин

В одном из следующих постов в цикле "Применение Docker" мы с вами будем разворачивать бота на сервере, но перед этим необходимо кое-что подготовить.

Когда мы прописывали токен бота в посте "AIOgram3 3. Основная функция", я упомянул, что хранение в коде секретных данных небезопасно. В данном посте мы исправим это, и я покажу, как правильно передавать секреты в бота.


Переменные окружения.
🔥3👍1
Переменные окружения (environment variables) – это механизм хранения данных, который позволяет хранить конфигурационные параметры вне кода. Они обычно используются для хранения секретной информации, такой, как ключи API, пароли и токены, а также для хранения прочих параметров (пути к файлам, настройки приложения и т.п.).

Для хранения переменных окружения с их значениями мы будем использовать специальный, "скрытый" .env файл. Название файла именно так и пишется &lt;точка&gt;env, без каких либо расширений. Самый известный пример подобных "скрытых" файлов, это .gitignore.

Обратите внимание. В системе может быть выключено отображение скрытых файлов.

Для получения данных из переменных окружения в коде будем использовать модуль environ из встроенной в Python библиотеки os.


Файл .env.
В корневой директории проекта, там, где файл main.py, создадим .env-файл и откроем его.

Откройте рядом файл settings.py и найдите класс Secrets.
Если вы повторяли все посты, то у вас должно быть шесть полей в этом классе: token, admin_id, group_id, weather_key, audio_key_id, audio_key_secret.

Создадим в .env-файле такие же строки, но в верхнем регистре, т.к. переменные окружения принято прописывать заглавными буквами. После каждой переменной ставим равно = и копируем значение из класса Secrets. Без кавычек.

Должно получиться вот так:
TOKEN=ваш_токен  
ADMIN_ID=id_администратора
GROUP_ID=id_группы
WEATHER_KEY=токен_погоды
AUDIO_KEY_ID=id_распознавания_голоса
AUDIO_KEY_SECRET=токен_распознавания_голоса


Если вы используете git, то обязательно внесите .env-файл в .gitignore!


Получение данных из окружения и .env-файла.
Когда мы будем разворачивать бота на сервере, все данные из .env-файла будут переданы внутрь контейнера, а самого его там не будет в целях безопасности. Однако, для работы с .env-файлом на локальной машине, необходимо его прочесть и получить из него данные.

Это можно сделать вручную, открыв файл и записав значения в словарь, но мне этот подход не нравится. К тому же всё написано до нас. Для чтения локального файла будем использовать библиотеку dotenv.

Для установки библиотеки необходимо выполнить команду:
pip install python-dotenv


После этого добавляем её в файл requirements.txt. Он нам понадобится во время разворачивания проекта на сервере.
python-dotenv1.0.1


Вернёмся к файлу settings.py.

В самом начале файла, сразу после импортов, вызываем функцию load_dotenv() для чтения .env-файла.
from dotenv import load_dotenv


load_dotenv()


Переходим к классу Secrets.
В нём нам необходимо заменить все значения полей на os.environ.get(""), где в кавычках указывается переменная окружения.

Обратите внимание! Если тип поля, например, int, то и значение полученное из переменной окружения нужно приводить к этому типу.

Получается так:
@dataclass  
class Secrets:
token: str = os.environ.get("TOKEN")
admin_id: int = int(os.environ.get("ADMIN_ID"))
group_id: int = int(os.environ.get("GROUP_ID"))
weather_key: str = os.environ.get("WEATHER_KEY")
audio_key_id: str = os.environ.get("AUDIO_KEY_ID")
audio_key_secret: str = os.environ.get("AUDIO_KEY_SECRET")



Завершение.
На этом подготовка закончена. Теперь на локальной машине секретные данные будут браться из .env-файла, а о том, как это будет работать на сервере, узнаете в следующих постах.
👍3🔥3
Приветствую!

В длинных постах можно запутаться поэтому, собираю воедино всё, что есть на данный момент.


Оглавления:
Для удобства навигации есть посты с оглавлениями по темам:

"Сайт на Django"
"Telegram-бот на AIOgram3"
"Применение Docker"
"Полезные инструменты"
"Путь в IT."
"Код в мешке"
"Boosty эксклюзив"


Ресурсы канала:

Уютный и немного безумный чат канала.
Бот с материалами к постам
Сайт со всеми постами
Канал в Dzen
Сообщество в VK


Поддержка.

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

Также поддержать канал можно на Boosty.

Или внеся сайт в исключения вашего блокировщика рекламы.
🔥3