All streams
Search
Write a publication
Pull to refresh
15
0
Иван Зайцев @IvanZaycev0717

Fullstack Developer, Москва

Send message

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

Кстати хорошо, что сказали. Я уже уточнил на заглушке, что происходит и когда открытие сайта.

В публичном репозитории все работает для запуска с локальной машине, но у сайта на сервере будет больше фич

Публичный ключ указываем в глобальных настройках GitHub, чтобы можно было клонировать репозиторий. А приватный ключ указываем в Secrets репозитория нашего проекта, чтобы можно было реализовать CI/CD через GitHub Actions.

Есть супер объяснение как эти ключи получать и что куда класть - https://www.youtube.com/watch?v=V2YYhGn3MGo&ab_channel=Chaby%27sTech

У нас приватный ключ будет использован в CI/CD

cd:
    needs: ci
    name: Update server buy ssh
    runs-on: ubuntu-latest
    steps:
      - name: Connect and run scripts
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.PORT }}
          script: |
            whoami
            cd ${{ secrets.PROJECT_FOLDER }}
            git checkout main
            git pull
            docker compose down
            docker builder prune --force
            docker image prune --force
            docker compose up -d --build

Конкретно в этом случае я использовал Flask, потому что, исходя из технического задания, более логичной архитектурой приложения будет монолит MPA. А такая архитектура отлично реализуется на Flask.

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

если ядер больше, то можно смело увеличивать количество воркеров по формуле

А почему бы нам не автоматизировать использование данной формулы для автоопределения количества дотсупных логических ядер на виртуальной машине. Например, с помощью такого скрипта gunicorn.conf.py

import fcntl
import multiprocessing
import socket
import struct


def get_ip_address(ifname):
    """Return current IP of our web app."""
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,
        struct.pack('256s', bytes(ifname[:15], 'utf-8'))
    )[20:24])


ip_address = get_ip_address('eth0')

bind = f'{ip_address}:8000' # Здесь свой порт выберите
workers = multiprocessing.cpu_count() * 2 + 1

timeout = 2
preload = True
loglevel = 'info'

Ну а дальше все очень просто:

gunicorn -c ./gunicorn.conf.py wsgi:app

  @classmethod
    async def find_all(cls):
        async with async_session_maker() as session:
            query = select(cls.model)
            result = await session.execute(query)
            return result.scalars().all()

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

ПОРАБОТАЕМ В СИНХРОННОЙ АЛХИМИИ

В обычной синхронной алхимии используется, как правило, либо execute для выполнения произвольных SQL-запросов и возвращает объект ResultProxy, либо scalars для выполнения запросов, которые возвращают ровно один столбец и одну строку, и возвращает значение этой единственной ячейки напрямую. То есть если бы это была обычная синхронная сессия, такой синтаксис был бы далеко не самым лучшим решиением.

Код для синхронной алхимии:

# импортируем то, что нужно
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# где-то ранее создаем движок и сессию
engine = create_engine(os.environ.get('DATABASE_URL'))
Session = sessionmaker(engine)

# теперь работаем в классе
@classmethod
def find_all(cls):
    with Session() as session:
        query = select(cls.model)
        return session.scalars(query)

У scalars не нужно писать all() - оно по умолчанию установлено

ТЕПЕРЬ ПОГОВОРИМ ОБ АСИНХРОННОЙ АЛХИМИИ

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

# импортируем все необходимое для работы
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

# создаем движок и сессию
engine = create_async_engine(os.environ.get('DATABASE_URL'))
Session = async_sessionmaker(engine, expire_on_commit=False)

# теперь работаем в классе
@classmethod
async def find_all(cls):
    async with Session() as session:
        query = select(cls.model)
        return await session.stream_scalars(query)

Метод session.stream_scalars(query) обеспечивают возврат асинхронной версии объекта, который поддерживает протокол асинхронной итерации Python.То есть для его итерации вы будете использовать не простой цикл for, а асинхронный async for . Он позволяет выполнять асинхронные операции в процессе итерации. Итерации выполняются в асинхронном контексте, что позволяет эффективно использовать время CPU при ожидании завершения асинхронных операций вместо блокировки процесса.

И на последок небольшую психологическую поддержку Вам хочу оказать: продолжайте дальше писать статьи, вы работаете с довольно сложными вещами. И официальный туториал по SQLAlchemy написан, слабо говоря, не лучшим образом. Книга только одна вменяемая по этой теме - вот эта. Ну а к хамству и грубости надо просто привыкнуть - как сказал не последний человек в мире Python Никита Соболев, что в России все отлично с хардами, но все ужасно с софтами. Грубость в комментариях к вам никакого отношения не имеет, этих людей так с детсва воспитало наше общество

При запуске Elasticsearch с помощью Docker команда для его запуска выглядит так:

$ docker run --name elasticsearch -d --rm -p 9200:9200 \    --memory="2GB" \    -e discovery.type=single-node -e xpack.security.enabled=false \    -t docker.elastic.co/elasticsearch/elasticsearch:8.11.1

Эта инструкция работать не будет работать для IP-адресов из России, так как docker.elastic.co заблокировало доступ для "дорогих россиян".

Но выйти из положения можно - здесь я использовал рамдомную версию Elasticsearch

Подтягиваем образ с DockerHub

docker pull elasticsearch:7.17.22

Далее создаем контейнер

docker run --name elasticsearch -d --rm -p 9200:9200 --memory="2GB" -e discovery.type=single-node -e xpack.security.enabled=false -t elasticsearch:7.17.22

В Docker Compose в YAML-файле это будет выглядеть так:

elasticsearch:
    image: elasticsearch:7.17.22
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    mem_limit: 2GB

Вообще очень хорошие статьи - всегда хочется немного освежить основы. Причем оригинальный автор Miguel Grinberg - довольно известный ирландский программист. Если встретите его книжку по SQLAlchemy 2.0 - советую ознакомиться, очень хорошо она написана

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

Приведу небольшой пример из современного GameDev: крупная корпорация Ubisoft сожрала многие студии и что мы видим - полную деградацию и чистый убыток в полмиллиарда евро. Крупные корпорации типа Microsoft выпустили на свет абсолютно провальный Redfall, после чего закрыли студию Arkane. Также другой крупный тайтл Starfield имеет "в основном отрицательные" недавние отзывы в Steam.

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

Немного подушню, но на диаграммах неплохо было бы проценты ставить.

Верно, но вы то в статьте пишете о том, что в многопоточном исполнении

Похоже вы не поняли для чего я это сделал: вы не заметили, что при выполнении запросов плавно меняется progress bar. При нажатии кнопки "Начать" мы будем с  помощью aiohttp отправлять запросы с  максимальной скоростью. Пока картинки скачиваются, мы можем отправлять команды обновления progress bar хода выполнения из цикла событий asyncio в цикл событий Tkinter.
Очень интересно посмотреть как вы сделаете отзывчивый UI через пул процессов?

А это неверно. С чего она будет заблокирована? Есть основной поток с tkinter и есть остальные потоки с отправкой запросов. И почему основной поток будет блокироваться?

Это вы вырвали из контекста статьи. Это было написано ДО того, как я заговорил о многопоточности. Т.е. если реализовать длительную блокирующую операцию в mainloop(), то программа "застынет" и ОС подумает, что она зависла. Если реализовать длительную блокирующую операцию в дополнительнм потоке - то он застынет на время выполнения операции, а главный поток будет нормально работать. Вы даже статью нормально прочитать не смогли, вам главное кое-что на вентилятор побыстрее накинуть

Какая такая? Код-ревью это вообще не про корпоративную культуру

Вы на Pull request такие же комментарии оставляете: "Зачем ты изобретаешь велоспед", "Хватит говнокодить"?

Знаете есть одна хорошая книга у Роберта Саттона пр то, с кем не надо работать. Судя по вашему тону вы мне именно таким и представляетесь.

Спасибо за комментарий. Я привел только один из способов реализации. Другой способ - в самом первом комментарии

В tkinter имеется собственный цикл событий mainloop(), который блокирует главный поток. Это означает, что любая длительная операция может привести к заморозке пользовательского интерфейса.
Избежать проблемы можно через выполнение функций, которые не блокируют этот цикл событий. Это достигается тем, что исполнение цикла событий в asyncio надо делать в отдельном потоке.
Про глобальную блокировку интерпритатора GIL на Хабре написан не один десяток статей.

Полнейшая чушь. Я могу спокойно в многопоточном режиме отправить 1000 запросов в тред пул и дальше юзать программу не дожидаясь окончания выполнения запросов. При этом разницы в скорости по сравнению с asyncio никакой не будет.

А в чем чушь полнейшая, если по вашим словам "разницы в скорости по сравнению с asyncio никакой не будет"? Это просто 2 разных подхода для решения одной и той же задачи

Короче простите, но этот вывод полная хрень. Автору было бы неплохо сначала разобраться в различии многопоточности и асинхронности прежде чем делать такие заявления

Здесь больше эмоций, чем аргументированной критики. В моём заявлении нет ничего, что противоречило бы правильной реализации поставленной задачи

Вместо того чтобы вытащить, например, через консоль разработчика API запрос с нормальным JSON-ответом

Конкретно для этого примера можно так сделать. Но представте, что у нас на сайт не предоставляет API. Я описал универсальный подход.

А еще автор любитель поговнокодить и применить антипаттерны.

Все мы иногда любители этого. По поводу говнокода, я процитирую одного своего товарища, который толи в шутку, толи в заправду говорил: "Говнокод очень сильно ускоряет коммерческую разработку".

Я написал это приложение в своё удовольствие, никакой коммерческой составляющей здесь и в помине нет. Здесь просто показано одно из множества решений данной проблемы. Эффективное оно или нет - вы решаете сами для себя.

А теперь от меня вопрос:

Интерестно, у вас на работе такая же корпоративная культура? Тогда я вам сочувствую

u префикс - это просто явное указание

Вот реальный пример генерации таких строк из QtDesigner с русским языком

self.checkBox.setText(QCoreApplication.translate("MainWindow", u"\u041e\u0434\u043d\u043e\u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043d\u043e\u0441\u0442\u044c", None))

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

А я бы стал

pip install pyside6
Collecting pyside6
  Obtaining dependency information for pyside6 from https://files.pythonhosted.org/packages/0c/e3/2a7155a4f776b50f88add0af1e6af2c976e5819ab4fb375cb811157aacd5/PySide6-6.6.2-cp38-abi3-win_amd64.whl.metadata
  Downloading PySide6-6.6.2-cp38-abi3-win_amd64.whl.metadata (5.5 kB)
Collecting shiboken6==6.6.2 (from pyside6)
  Obtaining dependency information for shiboken6==6.6.2 from https://files.pythonhosted.org/packages/74/46/00f867c025018148487a1138209513fbd4dcbae70faed052b027499f9c56/shiboken6-6.6.2-cp38-abi3-win_amd64.whl.metadata
  Downloading shiboken6-6.6.2-cp38-abi3-win_amd64.whl.metadata (2.6 kB)
Collecting PySide6-Essentials==6.6.2 (from pyside6)
  Obtaining dependency information for PySide6-Essentials==6.6.2 from https://files.pythonhosted.org/packages/a0/08/eaa29a1e01c13241aacab6bfcf15cf49ae026abeaa1dafc018b42909dbb1/PySide6_Essentials-6.6.2-cp38-abi3-win_amd64.whl.metadata
  Downloading PySide6_Essentials-6.6.2-cp38-abi3-win_amd64.whl.metadata (3.8 kB)
Collecting PySide6-Addons==6.6.2 (from pyside6)
  Obtaining dependency information for PySide6-Addons==6.6.2 from https://files.pythonhosted.org/packages/75/06/97aabf6fe31413616ff9d4165db4802e9991a97eaa04250e731a6cc5d6ab/PySide6_Addons-6.6.2-cp38-abi3-win_amd64.whl.metadata
  Downloading PySide6_Addons-6.6.2-cp38-abi3-win_amd64.whl.metadata (4.2 kB)
Downloading PySide6-6.6.2-cp38-abi3-win_amd64.whl (519 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 519.0/519.0 kB 2.2 MB/s eta 0:00:00
Downloading PySide6_Addons-6.6.2-cp38-abi3-win_amd64.whl (110.8 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 110.8/110.8 MB 4.2 MB/s eta 0:00:00
Downloading PySide6_Essentials-6.6.2-cp38-abi3-win_amd64.whl (77.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 77.2/77.2 MB 5.8 MB/s eta 0:00:00
Downloading shiboken6-6.6.2-cp38-abi3-win_amd64.whl (1.1 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 7.5 MB/s eta 0:00:00
Installing collected packages: shiboken6, PySide6-Essentials, PySide6-Addons, pyside6
Successfully installed PySide6-Addons-6.6.2 PySide6-Essentials-6.6.2 pyside6-6.6.2 shiboken6-6.6.2

[notice] A new release of pip is available: 23.2.1 -> 24.0
[notice] To update, run: python.exe -m pip install --upgrade pip

Ввёл команду так, как автор написал в статье. Где вы взяли такие цифры?

У PySide6/PyQt6 есть один интересный момент - размер скачиваемых с помощью pip - билиотек - 100Мб+. А представьте, что придется все это компилировать в exe. А нужен ли такой графический интерфейс, если я, например, использую пару радио-кнопок и одно поле ввода?

Я считаю, что Qt следует использовать только в том случае, когда нет подобного функционала в стандартной библиотеке Python - tkinter.

Генерация кода QtDesigner - это что-то с чем-то, если у вас русский язык в приложении. В сгенерированном py-файле присутсвует такая красота как u-строки, которую редко встретишь. Поэтому Qt в Python просто побаловаться можно, но не более того

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

import functools
import time
import requests
import asyncio

def my_timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        return result, end - start
    return wrapper


def async_timed():
    def wrapper(func):
        @functools.wraps(func)
        async def wrapped(*args, **kwargs):
            start = time.time()
            try:
                result = await func(*args, **kwargs)
            finally:
                end = time.time()
                total = end - start
                return result, total
        return wrapped
    return wrapper

@my_timer
def get_acces_token():
    result = requests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass'))
    return result

print(get_acces_token())


@async_timed()
async def get_acces_token_async():
    result = requests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass'))
    return result

async def main():
    token = await get_acces_token_async()
    print(token)

asyncio.run(main())

Давайте запустим и посмотрим на результаты

(<Response [200]>, 0.7680404186248779)
(<Response [200]>, 0.7761859893798828)

Синхронный код работает чуть быстрее, чем асинхронный.

Таким образом, get_acces_token можко сделать обычной функцией, а в access_token убрать await. Корутина, где есть access_token в любом случае будет блокироваться из-за requests

async def get_acces_token(Client_ID, Client_Secret, authorization_code):

В этой корутине отсутсвует оператор await, она будет работать синхронно, и не будет использовать основные преимущества асинхронности.

Что касаетяется статьи, задумка отличнейшая. Лет 5 назад такого джуна как вы с руками и ногами взяли бы. Да, увы курсы сломали рынок IT. Но рынок - это динамическая структура, поэтому, видимо, классические отклики - это утаревшая стратегия. Но какая-же новая?

Хочу отбросить ваши сомнения - приложение работает так, как задумывалось и правильно. Я решил выполнить чистый код приложения, убрав графический интерфейс, многопоточность, асинхронку. Вместо этого будет синхронный код и чистый пул процессов с функцией map. Результаты идентичны, что и в приложении. Можете сами проверить

import random
from random import randint
import time
from concurrent.futures import ProcessPoolExecutor
random.seed(1)

def insert_sort(arr: int) -> None:
    """Insertion sort algorithm."""
    N = len(arr)
    for top in range(1, N):
        k = top
        while k > 0 and arr[k - 1] > arr[k]:
            arr[k], arr[k - 1] = arr[k-1], arr[k]
            k -= 1

def get_test_data(_numbers_amount, _arrays_number):
    return [
            [
                randint(1, 1000) for n in range(_numbers_amount)
                ] for _ in range(_arrays_number)
            ]

if __name__ == "__main__":
    start = time.time()
    arr = get_test_data(30000, 5)
    for a in arr:
        insert_sort(a)
    end = time.time()
    print(end - start)

    arr = get_test_data(30000, 5)
    start = time.time()
    with ProcessPoolExecutor() as process_pool:
        process_pool.map(insert_sort, arr)
    end = time.time()
    print(end - start)

У меня результаты такие

183.37491822242737
37.8449387550354 - Выполнилось немного быстрее, т.к. турбо-буст у процессора отработал

отличный комментарий. Спасибо, что сказали про Thread._daemonic - здесь моя невнимательность. Что касается применения модуля asyncio: использовал для реализации asyncio.gather чтобы дождаться завершения всех вызовов в списке call_coros

Я проверил, заменив list comprehension в алгоритме быстрой сортировки на создание обычных списков с append'ами и сравнил - результаты практически одинаковые. Ну если уж такое замечание пришло, я поменяю на классическую реализацию, чтобы ни у кого никаких сомнений не оставалось

Information

Rating
Does not participate
Location
Москва и Московская обл., Россия
Registered
Activity

Specialization

Fullstack Developer
Python
Fastapi
JavaScript
Vue.js
SQL
MongoDB