• Какая идеальная структура каталога для картинок и почему?

    neatsoft
    @neatsoft
    Life is too short for bad software
    В качестве имён файлов можно использовать хэш содержимого, файлы распределять по подкаталогам, имена которых образованы первыми символами имен файлов. Пример:
    /34c/82a/416/34c82a416fca4cf3a1215ad5e8f2d782.jpg
    Ответ написан
    Комментировать
  • Как интегрировать Django Channels в Django используя докер?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Есть два варианта:
    1. Если нагрузка небольшая, можно использовать Daphne и для WSGI, и для ASGI запросов;
    2. Передавать контейнерам режим работы через environment variables (использовать entrypoint script, который будет запускать manage.py runserver, uwsgi, gunicorn, daphne, или manage.py runworker в зависимости от выбранного режима), запросы распределять между контейнерами в зависимости от пути или доменного имени.

    "По классике" статику отдаёт не Nginx, а S3 - AWS или его аналоги в проде, Minio в dev окружениях.

    Вместо Nginx советую использовать Traefik - он умеет находить контейнеры и роутить запросы к ним в зависимости от параметров, указанных в docker-compose.yml. Автоматический load balancing, letsencrypt без лишних телодвижений, очень просто конфигурируется, может обслуживать несколько разных проектов без дополнительных настроек (актуально для фрилансеров).

    #!/bin/sh
    
    set -o errexit
    set -o pipefail
    set -o nounset
    
    
    if [ $DJANGO_DEBUG == "on" ]; then
        ./manage.py runserver 0.0.0.0:8000
    elif [ $DJANGO_MODE == "worker" ]; then
        ./manage.py runworker
    else
        daphne -b 0.0.0.0 -p 8000 config.asgi:application
    fi
    Ответ написан
    1 комментарий
  • Как прочитать словарь из текстового файла?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Это не json, т.к. в json используются двойные кавычки. Прочитать такие данные можно с помощью модуля ast:

    #!/usr/bin/env python3
    import ast
    
    
    filename = 'file.txt'
    
    # open file
    with open(filename) as f:
        # read lines one by one
        for line in f:
            # remove semicolon and new line characters from the end of the line
            line = line.rstrip('\n;')
            # parse string
            data = ast.literal_eval(line)
            # print representation of the data
            print(repr(data))
    Ответ написан
    3 комментария
  • Обладает ли преимуществами RAML перед SWAGGER (Open Api) в 2018 году?

    neatsoft
    @neatsoft
    Life is too short for bad software
    RAML - top-down, Swagger - bottom-up, они совершенно разные.

    Использую Swagger для автоматического создания документации на основе кода и комментариев. Есть некоторые шероховатости, но в целом всё работает. Позволяет экономить уйму времени, и легко поддерживать документацию в актуальном состоянии.
    Ответ написан
    4 комментария
  • Установка Linux без клавы?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Нужно изучить раздел Automated Installation в документации выбранного дистрибутива.

    Использовал Preseed для автоматической установки Debian и Ubuntu на 3000 устройств (digital signage system) - работает исправно.
    Ответ написан
    3 комментария
  • VPS для личных нужд без доменного имени?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Если нет желания покупать домен, можно воспользоваться Dynamic DNS - duckdns.org.

    Автоматическая смена IP - это дополнительная функциональность Duck DNS, его вполне можно использовать как обычный домен с неограниченным количеством поддоменов, указав адрес вручную: www.rainbowtoster.duckdns.org, dev.rainbowtoster.duckdns.org, test.rainbowtoster.duckdns.org, etc.

    Описание принципов работы и список сервисов, предоставляющих DDNS, можно посмотреть на Gnu Tomorrow.

    Дополнение:
    Альтернативный вариант - бесплатные домены в зонах tk, ml, ga, cf, и gq на Freenom.
    Ответ написан
    Комментировать
  • Как автоматически мониторить фриланс-сайты?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Это в корне неправильный путь, часто ведущий к бану. На топовых биржах есть HR-ы, которые сами предлагают интересные проекты с хорошими рейтами, причём многие из этих проектов никогда не попадают в листинги. Поэтому усилия лучше потратить на прокачивание профиля до PFP (freelancer.com) или Top Rated (upwork.com).
    После этого для получения нового проекта достаточно установить флажок "доступен для заказов" в настройках профиля - различные варианты начинают валиться в чат с периодичностью раз в 15-30 минут. Из предложенного списка, как правило, совсем несложно выбрать хороший проект.
    Ответ написан
    1 комментарий
  • Счего начать изучение DevOps?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Базис:
    The Twelve-Factor App (перевод)

    Список необходимых инструментов и технологий:
    DevOps Roadmap
    Ответ написан
  • Какой язык/фреймворк выбрать?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Фреймворки нужны для упрощения и ускорения разработки - избавления от бойлерплейта и защиты от типичных ошибок. Можно ли всё тоже самое сделать вручную? Можно, но не нужно - большая часть времени уйдет на изобретение велосипедов, некоторые из которых будут медленными или небезопасными.

    По моему опыту, Django позволяет реализовывать типичные задачи вдвое быстрее, чем Laravel (использовал оба). Во многом это заслуга Python и сложившейся вокруг него экосистемы. Здесь выбор очевиден.

    VueJS скорее с ReactJS нужно сравнивать, а не с Angular, т.к. Angular это фреймворк, а VueJS и ReactJS - библиотеки. Все три помогают быстро и эффективно создавать фронтенд современных веб приложений, но делают это по разному. В качестве первого мягко (ненастойчиво) рекомендую изучить VueJS.

    p.s. Вне зависимости от выбора, не стоит заниматься веб-разработкой под windows. Стандартные среды - Ubuntu 18.04 (либо любой другой, но не слишком маргинальный дистрибутив) и MacOS.
    Ответ написан
    5 комментариев
  • Ресурсы для изучения ruby?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Не знаю контекста, но в 2018 году этот вопрос звучит не менее странно, чем просьба посоветовать книгу по FORTRAN или COBOL. Руби остался исключительно в легаси проектах, новые начинать на нём никто не будет (находясь в здравом уме и трезвой памяти).

    Python, JavaScript, Go - вот основной стек современной веб-разработки (пишу именно про веб разработку, т.к. в вопросе упомянуты рельсы, и т.к. веб разработка - это наиболее массовый на сегодня сегмент).

    Изучение языка - это инвестирование времени с целью получения дохода в будущем (1 год - джун, 3 года - мидл, 6 лет - сеньор, если не пинать балду). Руби - очень сомнительная инвестиция.
    Ответ написан
  • Работает ли telegram oauth в России?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Telegram login widget в России не работает, но авторизация через Telegram элементарно реализуется с помощью бота. При отправке команды бот вызывает веб-хук, передавая имя пользователя, в ответ отправляется одноразовая ссылка для входа на сайт. Бота нужно запускать на зарубежном хостинге, т.к. с него должен быть доступен api.telegram.org, пользователь может находиться где угодно.

    Вот пример на Django 2.0 (максимально упрощённый):
    1. Через @BotFather нужно создать нового бота, записать полученный token - <bot_token>
    2. Добавить команду /login с помощью /setcommands
    3. В консоли сгенерировать случайный uuid (uuidgen -r) - <webhook_token>
    4. Настроить веб-хук (вместо httpie можно использовать curl, wget, или любую другую подобную утилиту):
      http "https://api.telegram.org/bot<bot_token>/setWebhook?url=https://<domain_name>/bot/<webhook_token>/"

    5. Внести необходимые изменения в проект
    6. Если используется DatabaseCache, инициализировать таблицу в базе данных:
      ./manage.py createcachetable

    .

    settings.py
    ...
    TELEGRAM_BOT_ACCESS_TOKEN = <bot_token>
    TELEGRAM_BOT_WEBHOOK_TOKEN = <webhook_token>
    TELEGRAM_BOT_OTP_TIMEOUT = 120
    
    USE_X_FORWARDED_HOST = True
    
    CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
            'LOCATION': 'django_cache',
        }
    }
    ...


    urls.py
    ...
    from .views import BotView, LoginView
    
    
    urlpatterns = [
        ...
        path('bot/<uuid:token>/', BotView.as_view()),
        path('login/<uuid:otp>/', LoginView.as_view(), name='login'),
    ]


    views.py
    import json
    import requests
    import uuid
    
    from django.conf import settings
    from django.contrib.auth import login
    from django.contrib.auth.models import User
    from django.core.cache import cache
    from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect
    from django.urls import reverse
    from django.utils.decorators import method_decorator
    from django.views import View
    from django.views.decorators.csrf import csrf_exempt
    from django.views.generic import TemplateView
    
    ...
    
    @method_decorator(csrf_exempt, name='dispatch')
    class BotView(View):
        def post(self, request, token):
            if str(token) != settings.TELEGRAM_BOT_WEBHOOK_TOKEN:
                return HttpResponseNotFound()
    
            params = json.loads(request.body)
            message = params['message']
    
            if message.get('text') == '/login':
                user = message['from']
                chat_id = user['id']
                data = {
                    'username': user['username'],
                    'first_name': user.get('first_name', ''),
                    'last_name': user.get('last_name', ''),
                }
    
                otp = uuid.uuid4()
                cache.set(otp, data, settings.TELEGRAM_BOT_OTP_TIMEOUT)
                path = reverse('login', args=[otp])
                login_url = request.build_absolute_uri(path)
    
                url = 'https://api.telegram.org/bot{}/sendMessage?chat_id={}&text={}'.format(
                    settings.TELEGRAM_BOT_ACCESS_TOKEN,
                    chat_id,
                    login_url,
                )
    
                requests.get(url, timeout=10)
    
            return HttpResponse('')
    
    
    class LoginView(TemplateView):
        template_name = 'login.html'
    
        def dispatch(self, request, otp):
            self.data = cache.get(otp, {})
            return super().dispatch(request, otp)
    
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            context['data'] = self.data
            return context
    
        def post(self, request, otp):
            cache.delete(otp)
    
            username = self.data.get('username')
            if not username:
                return HttpResponseRedirect(request.path)
    
            user, created = User.objects.update_or_create(
                username=username,
                defaults={
                    'first_name': self.data.get('first_name', ''),
                    'last_name': self.data.get('last_name', ''),
                },
            )
            login(request, user)
    
            path = reverse('home')
            return HttpResponseRedirect(path)


    templates/login.html
    {% extends 'bootstrap4/bootstrap4.html' %}
    
    {% block bootstrap4_content %}
    
    <div class="container">
    
      <div style="padding:5rem; text-align:center;">
        {% if not data %}
          <h1>This url invalid or expired.</h1>
        {% else %}
          <h1>Login as {{ data.username }}</h1>
          <form action="" method="post">
            {% csrf_token %}
            <button type="submit" class="btn btn-primary btn-lg btn-block">
              Login
            </button>
          </form>
        {% endif %}
      </div>
    
    </div>
    
    {% endblock %}
    Ответ написан
    Комментировать
  • Как сделать синхронизацию клиента и сервера?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Soft delete - для фильтрации элементов использовать поле is_deleted (default=False), при удалении присваивать ему значение True, физически строки из базы не удалять (либо удалять после синхронизации со всеми клиентами). При обмене данными опираться на значение поля updated_at (в котором нужно хранить дату последнего редактирования / удаления).
    Ответ написан
    Комментировать
  • Как вы делаете транслитерацию текста в под url?

    neatsoft
    @neatsoft
    Life is too short for bad software
    pip install python-slugify

    >>> from slugify import slugify
    >>> slugify('Тестовый набор')
    'testovyi-nabor'
    Ответ написан
    Комментировать
  • Как в Django создать атрибут модели без добавления поля в БД?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Для этого можно использовать либо property:
    import os
    
    from django.db import models
    
    
    class Article(models.Model):
        title = models.CharField(
            max_length=200,
        )
        image = models.ImageField(
            blank=True,
            upload_to='images',
        )
    
        @property
        def thumbnail(self):
            if self.image:
                path, filename = os.path.split(self.image.url)
                name, ext = os.path.splitext(filename)
                filename = 'prefix_' + name + '_suffix' + ext
                return os.path.join(path, filename)


    либо функцию:
    import os
    
    from django.contrib import admin
    from django.utils.html import format_html
    
    
    @admin.register(Article)
    class ArticleAdmin(admin.ModelAdmin):
        list_display = (
            'title',
            'get_thumbnail',
        )
        fields = (
            'title',
            'get_thumbnail',
        )
        readonly_fields = (
            'get_thumbnail',
        )
    
        def get_thumbnail(self, obj):
            if obj.image:
                path, filename = os.path.split(self.image.url)
                name, ext = os.path.splitext(filename)
                filename = 'prefix_' + name + '_suffix' + ext
                return format_html('<img src="{}">', os.path.join(path, filename))
        get_thumbnail.short_description = 'Thumbnail'
        get_logo.admin_order_field = 'image'


    либо и то, и другое:
    import os
    
    from django.contrib import admin
    from django.utils.html import format_html
    
    
    @admin.register(Article)
    class ArticleAdmin(admin.ModelAdmin):
        list_display = (
            'title',
            'get_thumbnail',
        )
        fields = (
            'title',
            'get_thumbnail',
        )
        readonly_fields = (
            'get_thumbnail',
        )
    
        def get_thumbnail(self, obj):
            url = obj.thumbnail
            return format_html('<img src="{}">', url) if url else ''
        get_thumbnail.short_description = 'Thumbnail'
        get_logo.admin_order_field = 'image'
    Ответ написан
    Комментировать
  • Как перенести большую базу с одного сервера на другой?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Если даунтайм не принципиален, то нужно остановить базу, и воспользоваться одним из следующих вариантов:
    1. Скопировать файлы с базой на новый сервер с помощью rsync, и заменить ими имеющиеся. На новом сервере должна быть установлена точно такая же версия Postgres.
    2. Направить вывод pg_dump на старом сервере на вход pg_restore на новом через ssh:
    pg_dump -Fc db_name | ssh user@new_server "pg_restore -d db_name"


    В противном случае лучше нанять для этой операции опытного DBA.
    Ответ написан
    Комментировать
  • Как ускорить импорт из csv в бд?

    neatsoft
    @neatsoft
    Life is too short for bad software
    1. Итерировать строки по несколько сотен штук, и создавать соответствующие им объекты единым вызовом к bulk_create:
    import itertools
    
    def grouper(iterable, n, fillvalue=None):
        args = [iter(iterable)] * n
        return itertools.zip_longest(*args, fillvalue=fillvalue)
    
    ...
    
    limit = 250
    
    for index, items in enumerate(grouper(rows, limit)):
        items = (i for i in items if i) # remove empty rows added by grouper
    
        products = []
    
        for item in items:
            product = Product(
                price=item[0],
                shipping=item[1],
            )
            products.append(product)
    
        Product.objects.bulk_create(products)

    Все 15 миллионов объектов разом создавать не стоит - ничего хорошего из этого не выйдет.

    2. Если необходимо создавать зависимые объекты, использовать транзакции:
    from django.db import transaction
    
    ...
    
    limit = 250
    
    for index, items in enumerate(grouper(rows, limit)):
        with transaction.atomic():
            for item in (i for i in items if i):
                product = Product.objects.create(
                    price=item[0],
                    shipping=item[1],
                )
                # product can be used there to create another objects, e.g.:
                for color in item[2].split(','):
                    ProductColor.objects.create(
                        product=product,
                        color=color,
                    )

    Транзакции, в данном случае, обеспечивают существенный прирост производительности.

    3. Запустить несколько процессов, каждому из которых передать в параметрах с какой позиции он должен начать, и сколько строк обработать. Чтобы дочерним процессам не пришлось читать весь файл с самого начала, позицию лучше передавать не номером строки, а смещением.

    4. Скомбинировать 3 и 1 или 3 и 2.

    5. Модели в джанго - это просто таблицы в базе данных. Можно добавлять строки с помощью SQL запросов из отдельного скрипта на Python или Go.
    Ответ написан
    5 комментариев
  • Как изменять имя файла при загрузки в Django?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Для именования файлов лучше использовать хэш, а не uuid. Это гарантирует уникальность имён, и обеспечивает дедупликацию данных: сколько бы раз пользователи не заливали файл с одинаковым содержимым, он будет храниться в единственном экземпляре (исключение - файлы с одинаковым содержимым и разными расширениями, но это разумный компромисс - работать с файлами без расширений менее удобно на многих этапах).

    Можно реализовать соответствующую функциональность самостоятельно:

    app_name/models.py:
    import hashlib
    import os
    
    from django.conf import settings
    from django.core.files.storage import FileSystemStorage
    from django.db import models
    
    
    def upload_to(instance, filename, fieldname):
        ext = os.path.splitext(filename)[1].lower()
        class_name = instance.__class__.__name__.lower()
    
        h = hashlib.sha256()
        field = getattr(instance, fieldname)
        for chunk in field.chunks():
            h.update(chunk)
        name = h.hexdigest()
    
        return os.path.join(
            class_name,
            name + ext,
        )
    
    
    class OverwriteStorage(FileSystemStorage):
        def get_available_name(self, name, max_length=None):
            if self.exists(name):
                os.remove(os.path.join(settings.MEDIA_ROOT, name))
            return name
    
    
    class Article(models.Model):
        image = models.ImageField(
            storage=OverwriteStorage(),
            upload_to=lambda inst, fn: upload_to(inst, fn, 'image'),
        )


    Но гораздо разумнее воспользоваться готовой библиотекой:

    Установить hashedfilenamestorage:
    pip install django-hashedfilenamestorage

    и добавить следующую строку в settings.py:
    DEFAULT_FILE_STORAGE = 'django_hashedfilenamestorage.storage.HashedFilenameFileSystemStorage'
    Ответ написан
    1 комментарий
  • Как использовать Django аутентификацию с Amazon Gateway API?

    neatsoft
    @neatsoft
    Life is too short for bad software
    OAuth + Amazon Lambda function для проверки токенов
    API Gateway Custom Authorizers
    Использую в крупном проекте с большим количеством клиентов, несколько микросервисов с единым центром авторизации, всё работает как надо.
    Ответ написан
    2 комментария
  • Выбор монитора для программиста, как правильно?

    neatsoft
    @neatsoft
    Life is too short for bad software
    Народ, вы чего, блин? 1920 x 1080 для программиста в 2017 году? Уже давно доступны 4K (3840 x 2160) мониторы по вполне разумным ценам. Это совершенно другой уровень комфорта и продуктивности - можно одновременно четыре FullHD окна разместить, можно 1920x2160 (для кода) и два 1920x1080 (для браузера и консоли). Картинка гораздо более детализированная и чёткая. Обычные мониторы после UHD выглядят путешествием в прошлое.

    Использую три штуки LG 27UD88 (с общим разрешением 11520 x 2160), вполне доволен. Разумная диагональ, мерцание отсутствует при всех уровнях яркости, поддерживает DDC/CI (позволяет менять настройки на всех трёх мониторах с компьютера одновременно).
    Минусы: моргает светодиод в спящем режиме, появляются текстовые надписи в процессе загрузки (при переключении режимов).

    В качестве альтернативы рассматривал DELL P2715Q, но у него HDMI 1.4 (только 30 Гц в полном разрешении), нет USB Type-C (с поддержкой DisplayPort Alternate Mode), нарекания на дешевые материалы корпуса, и жалобы на зависания при выходе из спящего режима.

    LG 27UD58 и LG 27UD69P - бюджетные аналоги 27UD88 с урезанным набором портов. В 27UD58 нерегулируемая по высоте подставка.
    Ответ написан
    7 комментариев
  • Ubuntu Как запустить программу в фоне?

    neatsoft
    @neatsoft
    Life is too short for bad software
    byobu
    установка:
    sudo apt install byobu
    запуск:
    byobu
    перевод в фоновый режим:
    F6
    подключение к фоновому сеансу:
    byobu
    создание окна - F2
    переключение между окнами - F3 / F4
    закрытие окна - Ctrl+D
    Ответ написан
    1 комментарий