Русский
Русский
English
Статистика
Реклама

Sqlite

Перевод Применение SQLiteOpenHelper и Database Inspector в Android-разработке

19.04.2021 16:09:25 | Автор: admin
Автор статьи, перевод которой мы публикуем сегодня, хочет рассказать об использовании баз данных SQLite в Android-разработке. В частности он коснётся тут двух вопросов. Во-первых речь пойдёт о классе SQLiteOpenHelper, который применяется для работы с базами данных в коде приложений. Во-вторых он уделит определённое внимание инструменту Database Inspector, инспектору баз данных, встроенному в Android Studio.



Что такое SQLite?


SQLite это опенсорсная реляционная СУБД, похожая на MySQL. SQLite входит в состав стандартной библиотеки Android, где реализован движок базы данных, не нуждающийся в применении клиент-серверной архитектуры, не требующий особой настройки, поддерживающий транзакции. Для работы SQLite не нужен сервер баз данных. Всё, что нужно, хранится в обычных файлах. Полноценная БД SQLite, в которой имеется множество таблиц, триггеров, индексов и представлений, содержится в единственном, самом обыкновенном файле. Стандартная поддержка СУБД SQLite имеется во всех мобильных телефонах и в большинстве компьютеров. При этом работа с SQLite не требует решения задач по администрированию или настройке баз данных.

Android-разработчик, для выполнения CRUD-операций из своего приложения, может пользоваться пакетом android.database.sqlite.SQLiteDatabase, в котором реализованы все необходимые API.

Класс SQLiteOpenHelper


SQLiteOpenHelper это класс, встроенный в пакет android.database.sqlite.SQLiteDatabase. Это вспомогательный класс, который отвечает за создание баз данных SQLite и за управление их версиями. Для того чтобы воспользоваться возможностями этого класса, нужно создать его подкласс, в котором надо переопределить два метода onCreate() и onUpgrade(). Этот класс позволяет открывать существующие базы данных, создавать новые базы данных и обновлять версии баз данных.

Метод onCreate()


Метод onCreate() вызывается при создании базы данных. Он, в процессе жизненного цикла приложения, вызывается лишь один раз. А именно, его вызов производится при первом обращении к методу getReadableDatabase() или getWritableDatabase(). Эти методы принадлежат классу SQLiteOpenHelper.

В следующем примере показано создание экземпляра класса DatabaseHelper, являющегося наследником SQLiteOpenHelper:

DatabaseHelper dbh = new DatabaseHelper(getApplicationContext());

Вот код конструктора DatabaseHelper:

public DatabaseHelper(Context context) {super(context,dbName,null,version);context=this.context;}

Класс SQLiteOpenHelper вызывает метод onCreate() после создания базы данных и создания экземпляра класса SQLiteDatabase. Этот метод, напомним, вызывается лишь один раз, при создании базы данных:

@Overridepublic void onCreate(SQLiteDatabase db) {try {db.execSQL(CREATE_TABLE);} catch (Exception e) {}}

Метод onUpgrade()


Метод onUpgrade() вызывается в тех случаях, когда нужно обновить версию существующей базы данных:

@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {db.execSQL(DROP_TABLE);onCreate(db);}

Пример


Создадим простое Android-приложение и разберём практический пример работы с классом SQLiteOpenHelper. Это приложение, применяя данный класс, позволит нам добавлять записи о работниках некоей компании в таблицу Empdata базы данных Company.db.

Приложение имеет весьма простой интерфейс, описанный в файле activity_main.xml.


Интерфейс приложения

Вот содержимое файла MainActivity.java:

package www.sqliteopenhelper.sqliteopenhelper;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.Toast;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);EditText edtEmpName = findViewById(R.id.edtName);EditText edtDesig = findViewById(R.id.edtDesig);EditText edtSalary = findViewById(R.id.edtSalary);Button btnSave = findViewById(R.id.btnSave);DatabaseHelper dbh= new DatabaseHelper(getApplicationContext());btnSave.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {Employee objEmp = new Employee(edtEmpName.getText().toString(),edtDesig.getText().toString(),Integer.parseInt(edtSalary.getText().toString()));if(dbh.InsertEmployee(objEmp))Toast.makeText(getApplicationContext(),"Record inserted successfully",Toast.LENGTH_LONG).show();elseToast.makeText(getApplicationContext(),"Record not inserted",Toast.LENGTH_LONG).show();}});}}

Вот содержимое Employee.java:

package www.sqliteopenhelper.sqliteopenhelper;public class Employee {private String employeeName;private String employeeDesig;private int employeeSalary;public Employee() {}public Employee(String employeeName, String employeeDesig, int employeeSalary) {this.employeeName = employeeName;this.employeeDesig = employeeDesig;this.employeeSalary = employeeSalary;}public String getEmployeeName() {return employeeName;}public String getEmployeeDesig() {return employeeDesig;}public int getEmployeeSalary() {return employeeSalary;}public void setEmployeeName(String employeeName) {this.employeeName = employeeName;}public void setEmployeeDesig(String employeeDesig) {this.employeeDesig = employeeDesig;}public void setEmployeeSalary(int employeeSalary) {this.employeeSalary = employeeSalary;}}

Вот файл DatabaseHelper.java:

package www.sqliteopenhelper.sqliteopenhelper;import android.content.ContentValues;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;public class DatabaseHelper extends SQLiteOpenHelper {public static final int version = 1;public static String dbName="Company.db";public static final String TABLE_NAME ="Empdata";public static final String COL1 = "id";public static final String COL2 = "name";public static final String COL3 = "designation";public static final String COL4 = "salary";private static final String CREATE_TABLE="create table if not exists "+ TABLE_NAME + "(" + COL1 + " INTEGER PRIMARY KEY AUTOINCREMENT,"+COL2+" TEXT NOT NULL,"+ COL3 + " TEXT, " +COL4 + " INTEGER);";private static final String DROP_TABLE = "DROP TABLE IF EXISTS "+ TABLE_NAME;private Context context;public DatabaseHelper(Context context) {super(context,dbName,null,version);context=this.context;}@Overridepublic void onCreate(SQLiteDatabase db) {try {db.execSQL(CREATE_TABLE);} catch (Exception e) {}}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {db.execSQL(DROP_TABLE);onCreate(db);}public boolean InsertEmployee(Employee objEmp){SQLiteDatabase db=this.getWritableDatabase();ContentValues cv = new ContentValues();cv.put(COL2,objEmp.getEmployeeName());cv.put(COL3,objEmp.getEmployeeDesig());cv.put(COL4,objEmp.getEmployeeSalary());long result = db.insert(TABLE_NAME,null,cv);if(result == -1)return false;elsereturn true;}}

В коде DatabaseHelper.java видно, что класс DatabaseHelper является наследником класса SQLiteOpenHelper.

После того, как база данных создана, можно приступать к работе с ней. В частности создавать и обновлять поля её таблиц. При решении этих задач можно пользоваться объектом типа ContentValues, который позволяет хранить пары ключ-значение. Именно такие объекты используются для добавления новых записей в таблицы базы данных (с помощью метода insert() объекта SQLiteDatabase) и для обновления существующих записей (с помощью метода update()).

Database Inspector в Android Studio


Инструмент Database Inspector позволяет исследовать базы данных, используемые в приложениях, выполнять запросы к ним, модифицировать их, делая всё это во время работы приложений. Database Inspector имеется в Android Studio начиная с версии 4.1. Этот инструмент особенно полезен при отладке механизмов программ, ответственных за работу с базами данных. Database Inspector работает и с обычной реализацией SQLite, и с библиотеками, построенными на её основе, вроде Room. Database Inspector работает лишь с библиотекой SQLite, входящей в состав операционных систем Android, уровень API которых не ниже 26.

Для того чтобы открыть базу данных в Database Inspector нужно выполнить следующие действия:

  1. Запустите приложение в эмуляторе или на устройстве, подключённом к компьютеру. На эмуляторе или на устройстве должно присутствовать API не ниже 26 уровня.
  2. Выполните команду меню View > Tool Windows > Database Inspector.
  3. Выберите процесс выполняющегося приложения из выпадающего меню.
  4. В панели Databases появятся записи о базах данных, которые имеются в выбранном приложении. Теперь можно работать с этими базами данных.


Исследование базы данных с помощью Database Inspector

Обратите внимание на то, что есть одна проблема, связанная с аварийным завершением работы программ на эмуляторе Android 11 при подключении к Database Inspector. Если вы с этой проблемой столкнётесь здесь вы можете найти сведения о том, как с ней справиться.

Применяете ли вы SQLite в Android-разработке?
Подробнее..

Best practices для клиент-серверного проекта PoC

22.12.2020 12:11:10 | Автор: admin
image
Типичный проект PoC (Proof of Concept) состоит из клиента с GUI, сервера c бизнес логикой и API между ними. Также используется база данных, хранящая оперативную информацию и данные пользователей. Во многих случаях необходима связь с внешними системами со своим API.
Когда у меня возникла необходимость в создании проекта PoC, и я начал разбираться в деталях, то оказалось, что порог вхождения в веб-программирование весьма высок. В крупных проектах для каждого компонента есть выделенные специалисты: front-end, back-end разработчики, UX/UI дизайнеры, архитекторы баз данных, специалисты по API и информационной безопасности, системные администраторы. В небольшом PoC надо самому во всем разобраться, выбрать подходящее техническое решение, реализовать и развернуть. Ситуацию ухудшает тот факт, что из обучающих материалов не всегда понятно, почему предлагается сделать именно так, а не иначе, есть ли альтернативы, является ли решение best practice или это частное мнение автора. Поэтому я разработал заготовку под названием Common Test DB, отвечающую лучшим практикам. Ее можно использовать для начала любого проекта, остается только наполнить функциональным смыслом.
В статье я подробно опишу примененные best practices, расскажу про имеющиеся альтернативы и в конце размещу ссылки на исходники и работающий в сети пример.

Требования к проекту PoC


Начнем с минимальных требований к проекту PoC. В статье Блокчейн: что нам стоит PoC построить? я уже рассматривал из каких частей состоит универсальная информационная система. В этой статье подробно разберем состав типичного проекта PoC. Даже минимальный проект в клиент-серверной архитектуре требует значительной функциональности и технической проработки:

Клиент:
  • Должен осуществлять запросы к серверу, используя REST API
  • Регистрировать, авторизовать, аутентифицировать пользователей
  • Принимать, разбирать и визуализировать пришедшие данные

Сервер
  • Должен принимать HTTP/HTTPS запросы от клиента
  • Осуществлять взаимодействие с внешними системами
  • Взаимодействовать с базой данных (реализовать базовую функциональности CRUD)
  • Обеспечивать безопасность данных, вызовов API и конфигурационных параметров
  • Осуществлять логирование и мониторинг работоспособности

База данных
  • Необходимо реализовать структуру DB
  • Наполнить DB начальными значениями

Архитектура
  • Проект PoC должен быть реализован с возможностью расширения функциональности
  • Архитектура должна предусматривать масштабирование решения

Технологический стек


  • В настоящее время для большинства веб проектов используетсяязык JavaScript.Над JavaScript есть надстройкаTypeScript, которая обеспечивает типизацию переменных и реализацию шаблонов.
  • Для полнофункциональных веб проектов есть несколько стандартных стеков технологий. Один из самых популярных: MEVN (MongoDB + Express.js + Vue.js + Node.js), мне больше нравится PEVN (PostgreSQL + Express.js + Vue.js + Node.js), т.к. RDB базы мне технологически ближе, чем NoSQL.
  • Для GUI существует несколькофреймворков (Vue, React, Angular). Тут выбор, скорее, определяется традициями или личными предпочтениями, поэтому сложно говорить, что лучше, а что хуже. Сначала я выбрал Vue.js, да так на нем и остался. Этот фреймворк хорошо развивается, для него есть готовыевизуальныекомпоненты встилеMaterial Design (Vuetify), которые хорошо смотрятся даже при непрофессиональном применении. Считается, что для React порог вхождения выше, чем для Vue. В React сначала надо осознать специфичную объектную модель, которая отличается от классического веб программирования: компоненты как функции, компоненты как классы, цепочки вызовов.
  • Выбор базы данных уже сложнее, чем личные предпочтения. Но для PoC, зачастую, требуется не функциональность или производительность, а простота. Поэтому в примере будем использовать самую простую в мире базу SQLite. В промышленных PoC в качестве SQL базы я использую PostgreSQL, в качестве NoSQL ее же, т.к. запас прочности уPostgreSQL огромен. Прочность, конечно, не бесконечна и когда-нибудь настанет необходимость перехода на специализированную базу, но это отдельная тема для обсуждения.

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

Сервер


HTTP сервер


В качестве HTTP сервера япробовал два варианта:

Express более зрелый, Fastify, говорят, более быстрый. Есть еще варианты серверов, примеры перечислены в статье The Best 10 Node.js Frameworks. Я использовал самый популярный Express.js.

HTTPS запросы


Говоря про HTTP, я всегда подразумеваю HTTPS, т.к. без него крайне сложно построить безопасное взаимодействие между клиентом и сервером. Для поддержки HTTPS надо:
  • Получить SSL (TLS) сертификат
  • На сервере реализовать поддержку HTTPS протокола
  • На клиенте использовать запросы к клиенту с префиксом HTTPS

Получаем сертификат
Для организации работы HTTPS протокола нужен SSL сертификат. Существуют бесплатные сертификаты Lets Encrypt, но солидные сайты получают платные сертификаты от доверительных центров сертификации CA(Certificate Authority). Для наших тестовых целей на локальном хосте достаточно, так называемого, self-signedcertificate. Он генерируется с помощью утилиты openssl:
openssl req -nodes -new -x509 -keyout server.key -out server.crt

Далее отвечаем на несколько вопросов про страну, город, имя, email и т.п. В результате получаемдва файла:
  • server.key ключ
  • server.crt сертификат

Их нужно будет подложить нашему Express серверу.

HTTPS сервер
С Express сервером все достаточно просто, можно взять стандартный пакет HTTPS из Node.js, и использовать официальную инструкцию: How to create an https server?
После реализации HTTPS сервера используем полученные файлы server.key и server.crt и стандартный для HTTPS порт443.

Взаимодействие с внешними системами


Самая известная библиотека для реализации HTTP/HTTPS запросов: Axios. Она используется как в сервере для вызова API внешних систем, так и в клиенте для вызова API сервера. Есть еще варианты библиотек, которые можно использовать дляспецифичных целей: Обзор пяти HTTP-библиотек для веб-разработки.

Взаимодействие с базой данных


Для работы с базой данных я использовал самую популярную библиотеку для Node.js: Sequelize. В ней мне больше всего нравится то, что переключиться между различными типами баз данных можно всего лишь изменив несколько настроечных параметров. При условии, конечно, что в самом коде не используется специфика определенной базы.Например, чтобы переключиться с SQLight на PostgreSQL, надо в конфигурационном файле сервера заменить:
dialect: 'sqlite'

на
dialect: 'postgres'

И при необходимости изменить имя базы, пользователя и хост.
В PoC я использовал базу данныхSQLite, которая не требует установки.Для администрирования баз данных есть хорошо развитые GUI:

Безопасность доступа к данным


В примере реализована следующая простейшая логика доступа к данным, которую можно расширять и изменять в зависимости от бизнес-задач:
  1. При регистрации каждому пользователю присваивается уникальный ID.
  2. Когда пользователь создает новые данные, в них добавляется его ID.
  3. При любом запросе к данным проверяется, что пользователь имеет на это право:

  • Получить данные могут все зарегистрированные пользователи.
  • Изменить и удалить данные может только владелец этих данных.

Такая модель доступа к данным называетсяDiscretionary Access Control (DAC), по-русски: избирательное управление доступом. В этой модели владелец данных может делать с ними любые CRUD операции, а права остальных пользователей ограниченны.
Существуют несколько моделей контроля доступа к данным. ПомимоDiscretionary Access Control (DAC)OWASP рекомендует (Access Control Cheat Sheet) использовать следующие:
  • Role-Based Access Control (RBAC)
  • Mandatory Access Control (MAC)
  • Permission Based Access Control

Безопасность вызовов API


В моей предыдущей статье я рассматривал все возможные угрозы: Безопасность REST API от А до ПИ.В PoC реализованы следующие механизмы:
  • Cross-Site Request Forgery (CSRF) (Межсайтовая подмена запросов)

От данной угрозы реализован механизм CSRF токенов когда для каждой сессии пользователя генерируется новый токен (он же SessionId) и сервер проверяет его валидность при любых запросах с клиента. Алгоритм генерации и проверки SessionId подробно описан далее в разделе Авторизация, аутентификация пользователей.
  • Cross-origin resource sharing (CORS) (Кросс-доменное использование ресурсов)

Защита реализована с помощью пакета CORS. В config файле сервера указываются адрес (origin), с которого могут поступать API запросы и список методов (methods), которые может использовать клиент. В дальнейшем сервер будет автоматически ограничивать прием запросов в соответствии с этими настройками.
Для защиты от угроз, перечисленных далее, я использовал пакетhelmet, который позволяет задать значения для определенных HTTP заголовков:
  • Cross-site Scripting (XSS) (Межсайтовое выполнение скриптов)

Для защиты выставляется заголовок, ограничивающий выполнение скриптов:
X-XSS-Protection: 1; mode=block

  • Insecure HTTP Headers

Блокируется отправка сервером заголовка, дающего дополнительную информацию злоумышленнику:
X-Powered-By: Express

  • Insecure HTTP Headers: HTTP Strict Transport Security (HSTS)

Используется Strict-Transport-Security заголовок, который запрещает браузеру обращаться к ресурсам по HTTP протоколу, только HTTPS:
Strict-Transport-Security: max-age=15552000; includeSubDomains

  • Insecure HTTP Headers: X-Frame-Options (защита от Clickjacking)

Данный заголовок позволяет защититься от атаки Clickjacking. Он разрешает использовать фреймы только в нашем домене:
X-Frame-Options: SAMEORIGIN

  • Insecure HTTP Headers: X-Content-Type-Options

Установка заголовка X-Content-Type-Options запрещает браузеру самому интерпретировать тип присланных файлов и принуждает использовать только тот, что был прислан в заголовке Content-Type:
X-Content-Type-Options: nosniff

Защита от DoS


Необходимо защитить сервер и от отказа в обслуживании (DoS-атаки). Например, ограничить число запросов от одного пользователя или по одному ресурсу в течении определенного времени.
Для Node.js существуют средства, позволяющие автоматически реализовывать ограничения на число запросови сразу посылать ответ 429 Too Many Requests, не нагружая бизнес логику сервера.
В примере реализовано простейшее ограничение на число запросов от каждого пользователя в течении определенного времени по таблице Data. В промышленных системах надо защищать все запросы, т.к. даже один запрос, оставшийся без проверки может дать возможность провести атаку.

Безопасность конфигурационных параметров


Настройки для клиента и сервера хранятся в файлах configв JSON формате. Чувствительные настройки сервера нельзя хранить вconfig файлах, т.к. они могут стать доступны в системах версионного контроля. Для такой информации как: логины/пароли для базы данных, ключи доступа к API и т.д. надо использовать специализированные механизмы:
  • Задавать эти параметры в .env файле. Файл .env прописан в gitignore и не сохраняется в системах версионного контроля, поэтомупоэтому туда можно безопасно записывать чувствительную информацию.
  • Использовать механизм переменных окружения (environment variables), которые устанавливаются вручную или прописываютсяинсталляторами.

Логирование


Стандартные требования к логированию:
  • Обеспечить разный уровень логирования (Debug; Info; Warning; Error и т.д.)
  • Логировать HTTP/HTTPS запросы
  • Дублировать консольный вывод в файлы на диске
  • Обеспечить самоочистку файлов логирования по времени иобъёму

Варианты пакетов для логирования, которые я пробовал:

В проекте я использовал log4js. Пакет log4jsработает интуитивно понятно и достаточно развит, чтобы реализовать все требования.

Мониторинг


Мы не будем останавливаться на внешних система мониторинга: Обзор систем мониторинга серверов, т.к. они начинают играть важную роль не на этапе PoC, а в промышленной эксплуатации.
Обратимся к внутренним системам, которые позволяют наглядно посмотреть, что сейчас происходит с сервером, например пакетexpress-status-monitor. Если его установить, топо endpoint
/monitor

можно наглядно мониторить простейшие параметры работы сервера, такие как загрузка CPU, потребление памяти, http нагрузку и т.п. Приведу скриншот из нашего примера. На скриншоте горит красная плашка Failed и может показаться, что что-то не так. Но, на самом деле, все в порядке, т.к. вызов API сознательно делается на несуществующий endpoint:
/wrongRoutePath

image

База данных


Структурабазы данных


В нашем примере реализованы две таблицы с говорящими именами:
  • Users
  • Data

Лучшие практики говорят, что все проверки надо помаксимуму отдавать базе данных. Поэтому в схеме базы данных аккуратно определяем все типы данных, добавляем ограничения (constraints) и внешние ключи.Для пакета sequelize данная функциональность подробно описана в стандартной документации.
В нашем пример сделан один внешний ключ UserId в таблице Data для того, чтобы гарантировать, что у каждой записи в таблицеData будет определенный владелец User. Это позволит при изменении или удалении записи в Data реализовать проверку пользователя, т.к. по нашей логике данные может удалять только их владелец.
Еще одна особенность нашей схемы в том, что один столбец таблицы Data задан в формате JSON. Такой формат удобно использовать, если внешняя система возвращает данные в JSON. В этом случае можно просто записать полученные данные, не тратя усилия на парсинг,а потом делать SQL запросы, используя расширенный синтаксис.Описание работы со столбцами JSON можно найти в официальной документации баз данных. В качестве независимого описания мне понравилась статья на Хабре, в которой описаны все варианты запросов: "JSONB запросы в PostgreSQL.

Формальная схема DB
Cхема таблицыUsers:
  • id INTEGERPRIMARY KEY AUTOINCREMENT уникальный ID
  • uuid UUID NOT NULL UNIQUE уникальный UUID, по нему идет связь с данными этого пользователя в таблице Data
  • email VARCHAR(255) NOT NULL UNIQUE
  • password VARCHAR(64)
  • ddosFirstRequest DATETIME
  • ddosLastRequest DATETIME
  • ddosRequestsNumber DECIMAL
  • lastLogin DATETIME
  • loginState VARCHAR(255) NOT NULL
  • sessionId VARCHAR(1024)
  • commonToken VARCHAR(1024)
  • googleToken VARCHAR(1024)
  • googleAccessToken VARCHAR(1024)
  • googleRefreshToken VARCHAR(1024)
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL

Схема таблицыData:
  • id INTEGERPRIMARY KEY AUTOINCREMENT
  • uuid UUID NOT NULL UNIQUE
  • ownerUuid UUID NOT NULL
  • data JSON NOT NULL
  • UserId INTEGER REFERENCES Users (id) ON DELETE CASCADE ON UPDATE CASCADE,
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL


Начальные значения


  • В таблице Users задан один пользовательuser@example.comс паролем password.
  • В таблице Data записано несколько предопределенных значений, которые можно получить из GUI.

Далее перейдем к клиенту.

Клиент


HTTPS запросы


Для безопасной передачи информации клиент использует HTTPS протокол. Для запросов используется та же библиотека, что и на сервере для вызова API внешних систем: Axios.

RESTful API


REST API лучше реализовывать, используя стандартные средства, чтобы было проще тестировать и документировать, например SwaggerHub
Наше REST API сформировано по правилам Open API и использует стандартные HTTP методы для манипуляции объектами из таблиц Users и Data.В схеме нашего API не все сделано по чистым правилам REST, но главное соблюдена общая концепция.
Хорошей практикой являетсяограничение объёма возвращаемых данных. Для этого используются параметры в запросе: фильтр (filter), ограничение (limit) и смещение (offset). В примере этого нет, но в промышленных системах эта функциональность должна быть реализована.Но даже если реализованы ограничивающие механизмы на клиенте, должна осуществляться дополнительная проверка на сервере на максимальные значения. В нашем примере в конфигурации сервера реализован ограничитель на максимальное число возвращаемых строк из базы,который подставляется вSELECT запросы:limit: 1000

YAML файл с Open API описанием
---swagger: "2.0"info:  description: "This is a common-test-db API. You can find out more about common-test-db\    \ at \nhttps://github.com/AlexeySushkov/common-test-db\n"  version: "2.0.0"  title: "common-test-db"  contact:    email: "alexey.p.sushkov@gmail.com"  license:    name: "MIT License"    url: "https://github.com/AlexeySushkov/common-test-db/blob/main/LICENSE"host: "localhost:443"basePath: "/commontest/v1"tags:- name: "data"  description: "Everything about Data"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"- name: "users"  description: "Everything about Users"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"schemes:- "https"paths:  /data:    get:      tags:      - "data"      summary: "Gets all data"      description: "Gets all data"      operationId: "getData"      produces:      - "application/json"      parameters: []      responses:        "200":          description: "successful operation"          schema:            type: "array"            items:              $ref: "#/definitions/Data"        "400":          description: "Invalid status value"      x-swagger-router-controller: "Data"    post:      tags:      - "data"      summary: "Add a new data to the db"      operationId: "addData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data object that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "500":          description: "Create data error"      x-swagger-router-controller: "Data"    put:      tags:      - "data"      summary: "Update an existing data"      operationId: "updateData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data information that needs to be changed"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Update data error"      x-swagger-router-controller: "Data"    delete:      tags:      - "data"      summary: "Delete an existing data"      operationId: "deleteData"      consumes:      - "application/x-www-form-urlencoded"      - "application/json"      produces:      - "application/json"      parameters:      - name: "uuid"        in: "formData"        description: "uuid"        required: true        type: "string"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Delete data error"      x-swagger-router-controller: "Data"  /users:    post:      tags:      - "users"      summary: "Register new user"      operationId: "userRegister"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be deleted"        required: true        schema:          $ref: "#/definitions/User"      responses:        "500":          description: "Register user error"      x-swagger-router-controller: "Users"    put:      tags:      - "users"      summary: "Update existing user"      operationId: "userUpdate"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User information that needs to be changed"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"    delete:      tags:      - "users"      summary: "Delete user"      operationId: "userDelete"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"  /users/login:    post:      tags:      - "users"      summary: "Login"      operationId: "userLogin"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Login user error"      x-swagger-router-controller: "Users"definitions:  Data:    type: "object"    required:    - "Counter1"    - "Counter2"    properties:      Counter1:        type: "integer"        format: "int64"      Counter2:        type: "integer"        format: "int64"    example:      Counter1: 10      Counter2: 20  User:    type: "object"    required:    - "email"    - "password"    properties:      email:        type: "string"      password:        type: "string"    example:      password: "password"      email: "email"  ApiResponse:    type: "object"    properties:      status:        type: "string"


Интересная возможность встроить Swagger прямо в сервер, далее подложить ему YAML, реализующий API и по endpoint:
/api-docs

получить стандартную Swagger панель управления и осуществлять тестирование и просмотр документации. Скриншот Swagger, встроенного в наш сервер:

image

GraphQL


Существуют и альтернативы REST API, например GraphQL. Данный стандарт разработан Facebook и позволяет избавиться от недостатков REST:
  • В REST API взаимодействие происходит с использованием многочисленные endpoints. В GraphQL одна.
  • В REST API для получения данных от разных endpoints необходимо осуществить множественные запросы. В GraphQL все необходимые данные можно получить одним запросом.
  • В REST API клиент не может контролировать объём и содержимое данных в ответе. В GraphQL набор возвращаемых данных предсказуем, т.к. задается в запросе. Это сразу избавляет от угрозы: Excessive Data Exposure (Разглашение конфиденциальных данных)
  • В REST API отсутствует обязательность формальной схемы. В GpaphQL создание схемы API с определенными типами данных обязательно, при этом автоматически получается документация и сервер для тестирования (самодокументироваемость и самотестируемость).
  • Клиент всегда может запросить схему API с сервера, тем самым синхронизировать формат своих запросов
  • Из коробки работают подписки, реализованные на технологии WebSockets

Какое API использовать, решается в зависимости от бизнес задач. REST API практически безальтернативно используется в промышленных системах и задачах интеграции между ними. Но для проектов, не требующих взаимодействия со сторонними системами вполне можно использовать более продвинутый GraphQL.

Регистрация,аутентификация и авторизация пользователей


Если приложение доступно из Интернета, то в форме регистрации имеет смысл использовать функциональность reCaptсha от Google для защиты от ботов и DoS атак. В нашем примере я использую пакет vue-recaptcha.
Для использованияreCaptсha нужно на сайте Google зарегистрировать приложение, получить sitekey и прописать его в настройках клиента. После этого на форме появится, известный всем, вопрос про робота:
image

В примере реализована регистрация пользователей с помощью логина/пароля ис использованием Google Account.
  • При регистрации по логин/пароль пароли в базе данных, разумеется, не хранятся в виде текста, а хранится только хеш. Для его генерации используется библиотекаbcrypt, которая реализует алгоритм хеширванияBlowfish. Сейчас уже есть мнение, что надо использовать более надежные алгоритмы: Password Hashing: Scrypt, Bcrypt and ARGON2.
  • При регистрации с помощью Google Account организация хранения аутентификационных данных проще, т.к. в базу записывается только token, полученный от Google.

Для реализации различных схем аутентификации есть библиотека Passport, которая упрощает работу и скрывает детали. Но чтобы понять как работают алгоритмы, в примере все сделано в соответствии со стандартами, которые я уже описывал в своей статье: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn.
Далее в статье разберем практическую реализацию.

Аутентификация по логину/паролю (Basic Authentication)
Тут все достаточно просто. При получении запроса на логин сервер проверяет, что пользователь с присланнымemail существует и что хеш полученного пароля совпадает с хешом из базы. Далее:
  • Сервер формирует SessionId в формате UUID и Token в формате JWT (JSON Web Token)
  • Токену присваиватся время жизни, которое определяется бизнес задачами. Например, Google устанавливет своим токенам время жизни 1 час.
  • Для формирования токена используется пакетjsonwebtoken
  • В JWT помещается email пользователя, который подписывается ключом сервера. Ключ это текстовая строка, которая хранится, как конфигурационный параметр на сервере.Содержимое токена кодируется Base64
  • SessionId сохраняется в базе и отправляется клиенту в заголовке X-CSRF-Token
  • Token сохраняется в базе и отправляется клиенту в теле HTTP ответа
  • Получив SessionId клиент сохраняетSessionId в sessionStorageбраузера
  • Token записывается в localStorage или Сookiesбраузерадля реализации возможности запомнить меня на этом устройстве
  • В дальнейшем клиент будет присылать SessionId в HTTP заголовкеX-CSRF-Token:

X-CSRF-Token: 69e530cf-b641-445e-9866-b23c492ddbab

  • Token будет присылаться в заголовкеHTTP Authorization с префиксомBearer:

Authorization: BearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

  • Сервер каждый раз проверяетSessionId на то, что сессия существует и связана с пользователем. Таким образом, гарантируется, что запрос приходит от залогиненного клиента, а не стороннего сайта злоумышленника.
  • Сервер при каждом запросе будет проверять присланныйToken на валидность:

  1. Токен подписан сервером;
  2. Пользователь с email существует;
  3. Время жизни токена не истекло. Если же оно истекло, то при выполнении предыдущих условий автоматически формируется новый токен.

В общем случае излишне генерировать и SessionId и Token. В нашем PoC для примера реализованы оба механизма. Посмотрим внимательней на наш JWT. Пример закодированного JWT токена из заголовка:
Authorization:yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

Раскодируем его открытыми средствами. Например самый простой способ загрузить на сайтhttps://jwt.io

image

Мы видим, что информация, помещенная вJWT не зашифрована, а только подписана ключом сервера и туда нельзя помещать чувствительные к разглашению данные!

Аутентификация с помощью Google Account (Token-Based Authentication)
Тут посложнее, поэтому нарисую диаграмму:

image

1. Пользователь заходит на GUI PoC, выбирает Login with Google".
2. Клиент запрашивает у сервера SessionId и настройки Google. Настройки надо предварительно получить с сайта Google, например, по инструкции из моей статьи: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn
3. Клиент сохраняет SessionId в sessionStorageбраузера
4. Из браузера формируется GET запрос на GoogleAuthorization Server (красная стрелка). Для понимания алгоритма, надо обратить внимание, что ответ на этот запрос браузер получит от Google только в самом конце call flow на шаге 13 и это будет 307 (Redirect).

Формат GET запроса:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&scope=openid+email&redirect_uri=http://localhost:8081/redirect&access_type=offline&state=450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58

В этом запросе:
  • accounts.google.com/o/oauth2/v2/auth endpoint для начала аутентификации. У Google есть, кстати, адрес, по которому можно посмотреть актуальный список всех Google API endpoints:https://accounts.google.com/.well-known/openid-configuration
  • response_type=code параметр говорит, что ожидаем получить в ответAuthorization Code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • scope=openid email к каким данным пользователя мы хотим получить доступ
  • redirect_uri = localhost:8081/redirectCallback адрес, заданный при регистрации приложения. В нашем случае это адрес на нашем сервере, который получит запрос от Google
  • state = SessionId, который передается между клиентом, Google и сервером для защиты от вмешательства внешнего злоумышленника

5. Google Authorization Server показывает стандартную Google форму логина.
6. Пользователь вводит свои логин/пароль от Google аккаунта.
7. Google проверяет пользователя и делает GET запрос на адрес Callback с результатом аутентификации, Authorization Code в параметре code и нашем SessionId в параметреstate:
  • state: '450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58',
  • code: '4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ',
  • scope: 'email openid www.googleapis.com/auth/userinfo.email',
  • authuser: '1',
  • prompt: 'consent'

8. Сервер, не отправляя пока ответ на GET, формирует POST запрос с Authorization Code, Client ID и Client Secret:
POSThttps://oauth2.googleapis.com/token?code=4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&client_secret=SUmydv3-7ZDTIh8аНК85chTOt&grant_type=authorization_code&redirect_uri=http://localhost:8081/redirect

  • oauth2.googleapis.com/token это endpoint для получения token
  • code только что присланный code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • client_secret Client Secret, выданный при регистрацииприложения на Google (в примере указан не настоящий)
  • grant_type=authorization_code единственно возможное значение из стандарта

9. Google проверяет присланные данные и формирует access token в формате JWT (JSON Web Token), подписанный своим приватным ключом. В этом же JWT может содержаться и refresh token, c помощью которого возможно продолжение сессии после ее окончания:
  • access_token: 'ya29.a0AfH6SMBH70l6wUe1i_UKfjJ6JCudA_PsIIKXroYvzm_xZjQrCK-7PUPC_U-3sV06g9q7OEWcDWYTFPxoB1StTpqZueraUYVEWisBg46m1kQAtIqhEPodC-USBnKFIztGWxzxXFX47Aag',
  • expires_in: 3599,
  • refresh_token: '1//0cAa_PK6AlemYCgYIARAAGAwSNwF-L9IrdUt1gzglxh5_L4b_PwoseFlQA1XDhqte7VMzDtg',
  • scope: 'openid www.googleapis.com/auth/userinfo.email',
  • token_type: 'Bearer',
  • id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlZGMwMTJkMDdmNTJhZWRmZDVmOTc3ODRlMWJjYmUyM2MxOTcfobDCf2VrxXb6CCxoL_dZq1WnlEjBZx_Sf6Rg_tn3x4gWtusO1oe_bJx_gvlSLxtvSOdO_kPB2uGGQHr3xzF_Evr-S-BiGS8zMuIkslyN6fU7P7BdNVyOYAIYFvHikyIpAoesV2Fd2yBSngBwGmWfrHL7Z2415UrnlCG4H1Nw'

10. Сервер валидирует пришедший Token. Для этого нужен открытый ключ от Google. Актуальные ключи находятся по определенному адресу и периодически меняются:https://www.googleapis.com/oauth2/v1/certs.Поэтому надо или получать их каждый раз по этому адресу, либо хардкодить и следить за изменениями самому.Декодируем пришедший token, для нас самое главное это подтвержденный email пользователя:
decodedIdToken: {iss: 'https://accounts.google.com',azp: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',aud: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',sub: '101987547227421522632',email: 'work.test.mail.222@gmail.com',email_verified: true,at_hash: 'wmbkGMnAKOnfAKtGQFpXQw',iat: 1606220748,exp: 1606224348}

11. Проверяем, что email пользователя существует, или создаём нового пользователя c email из токена. При этомSessionId и полученные от Google токены сохраняются в базе.
12. Только теперь отвечаем Google HTTP кодом 307 (Redirect) и заголовком Location c адресом на клиенте:
HTTP Location:http://localhost:8080/googleLogin

13. И только теперь Google отвечает браузеру с тем же кодом307 (Redirect) и заголовком Location с заданным нами адресом
14. Браузер переходит на адрес, указанный вLocation иклиент определяет, что произошла успешная аутентификация пользователя с помощью Google аккаунта
15. Клиент, по сохраненному в sessionStorage SessionId, получает на сервере токен и данные пользователя
16. Клиент сохраняет токен в localStorage браузера

На этом процедура аутентификации с помощью Google аккаунта завершена и можно переходить к штатной работе приложения.

Прием, обработка и визуализация данных


Сделаем стандартный вид приложения, как рекомендует Google:
  • Drawer (navigation-drawer) в левой стороне
  • Меню сверху (v-app-bar)
  • Footer внизу (v-footer)
  • Для визуализации полученных данных используем карточки (Data Cards)

image

Для создания новых данных используется простая форма ввода:

image

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

  1. Для Vue есть библиотекаVuex, реализующаяstate management pattern.
  2. Есть универсальная библиотека Redux основанная на Flux от Facebook. Она реализует концепцию состояний, используя понятия action, state, view.

  • В примере я использовал Vuex.
  • Также необходимRouter для реализации переходов между страницами.

Мой опыт показывает, что для создания стандартных графических интерфейсов подходят стандартные средства, позволяющие делать приличные интерфейсы, даже без наличия дизайнерских навыков.

Архитектура


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

Заключение


  • Мы рассмотрели типичной проект PoC в архитектуре клиент-сервер для использования в вебе.
  • Поняли, что даже минимальный проект требует обширных знаний и значительных усилий для реализации.
  • В результате получили заготовку, которую можно использовать для начала любых проектов:

Исходники на Github:https://github.com/AlexeySushkov/common-test-db
Пример запущен в облаке Amazon, можно посмотреть, как он выглядит вживую:http://globalid.tech/

Остается только в канун Нового Года пожелать, чтобы сбылись все ваши самые заветные PoC!

It's only the beginning!
Подробнее..

Модуль для работы с sqlite3

07.05.2021 16:08:17 | Автор: admin

Сегодня Я хотел бы поделиться своей не большой разработкой, которая помогает мне уже более чем пол года: "Модуль для работы с sqlite3".

Концепция

Вместо написания SQL запросов мы будем передавать ключи, значения, названия таблиц, условия и callback'и, которые будут вызывать по завершению запросов(в каждый callback мы будем передавать ошибку и результат, если такой есть).

Представим модуль в виде класса.

Всего будет 4 метода:

  1. getData() - для получения данных из таблицы.

  2. insertData() - для добавления данных в таблицу.

  3. updateData() - для обновления данных в таблице.

  4. deleteData() - для удаления данных из таблицы.

Конечно же с помощью 4 методов приведенных выше мы не сможем исключить все виды запросов, но в моем случаи эти запросы самые частые.

Кодим

Для начало создадим сам класс, настроем экспорт и подключимся к БД и создадим метод, который будет добавлять кавычки у элемента, если тип элемента соответствует типу строки.

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

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

Начинаем собирать запрос с ключевого слова "SELECT", далее перебираем все столбцы, которые нам передали и добавляем их, но не забываем, что символ "*" не нужно "заковывать" в кавычки, т к этот символ говорит нам, что нужно получить все столбцы. Заканчиваем собор указанием таблицы и условия. Далее, исходя из кол-во строк, которые нам надо вернуть вызываем соответствующий метод.

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');        /**     *      * @param {String[]} keys      * @param {String} table      * @param {String} condition      * @param {Boolean} some      * @param {Function()} callback      */    static getData(keys, table, condition = '', some = true, callback = () => {}) {        let sql = 'SELECT ';        for (let i = 0; i < keys.length; i++) {            sql += keys[i] === '*' ? keys[i] : '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ' FROM `' + table + '` ' + condition;                if (some)            this.database.all(sql, (err, rows) => {                callback(err, rows);            });        else            this.database.get(sql, (err, row) => {                callback(err, row);            });    };    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

Напишем метод отвечающий за обновление данных.

Начинаем с указания таблицы, установкой ключей и значений, а завершаем добавлением условия.

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');        /**     *      * @param {String[]} keys      * @param {String} table      * @param {String} condition      * @param {Boolean} some      * @param {Function()} callback      */    static getData(keys, table, condition = '', some = true, callback = () => {}) {        let sql = 'SELECT ';        for (let i = 0; i < keys.length; i++) {            sql += keys[i] === '*' ? keys[i] : '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ' FROM `' + table + '` ' + condition;                if (some)            this.database.all(sql, (err, rows) => {                callback(err, rows);            });        else            this.database.get(sql, (err, row) => {                callback(err, row);            });    };        /**     *      * @param {String[]} keys      * @param {Values[]} values      * @param {String} table      * @param {String} condition      * @param {Function()} callback      */    static updateData(keys, values, table, condition, callback = () => {}) {        let sql = 'UPDATE `' + table + '` SET ';        for (let i = 0; i < keys.length; i++) {            sql += '`' + keys[i] + '` = ' + this.ToString(values[i]);            if (keys.length > i + 1)                sql += ', ';        }        sql += ' ' + condition;                this.database.run(sql, (err) => {            callback(err);        });    }    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

Остается совсем чуть-чуть, напишем метод для удаления данных(она максимально простой) и метод для добавления данных.

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

class DataBase {    /**     *      * @readonly     */    static sqlite3 = require('sqlite3').verbose();        /**    *     * @readonly    */   static database = new this.sqlite3.Database('./database/database.db');        /**     *      * @param {String[]} keys      * @param {String} table      * @param {String} condition      * @param {Boolean} some      * @param {Function()} callback      */    static getData(keys, table, condition = '', some = true, callback = () => {}) {        let sql = 'SELECT ';        for (let i = 0; i < keys.length; i++) {            sql += keys[i] === '*' ? keys[i] : '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ' FROM `' + table + '` ' + condition;                if (some)            this.database.all(sql, (err, rows) => {                callback(err, rows);            });        else            this.database.get(sql, (err, row) => {                callback(err, row);            });    };        /**     *      * @param {String[]} keys      * @param {Values[]} values      * @param {String} table      * @param {String} condition      * @param {Function()} callback      */    static updateData(keys, values, table, condition, callback = () => {}) {        let sql = 'UPDATE `' + table + '` SET ';        for (let i = 0; i < keys.length; i++) {            sql += '`' + keys[i] + '` = ' + this.ToString(values[i]);            if (keys.length > i + 1)                sql += ', ';        }        sql += ' ' + condition;                this.database.run(sql, (err) => {            callback(err);        });    }        /**     * @param {String[]} keys     * @param {String[]} values     * @param {String} table      * @param {Function()} callback      */    static insertData(keys, values, table, callback = () => {}) {        let sql = 'INSERT INTO `' + table + '` (';        for (let i = 0; i < keys.length; i++) {            sql += '`' + keys[i] + '`';            if (keys.length > i + 1)                sql += ', ';        }        sql += ') VALUES (';        for (let i = 0; i < values.length; i++) {            sql += this.ToString(values[i]);            if (values.length > i + 1)                sql += ', ';        }        sql += ')';        this.database.run(sql, (err) => {            callback(err);        });    };    /**     *      * @param {String} table      * @param {String} condition      * @param {Function()} callback      */    static deleteData(table, condition = '', callback = () => {}) {        this.database.run('DELETE FROM `' + table + '` ' + condition, (err) => {            callback(err);        });    }    static ToString(value) {        return typeof(value) === 'string' ? '\'' + value + '\'' : value;    }}module.exports = {    database: DataBase};

На этом все, спасибо за внимание!
Проект на GitHub

Подробнее..

Kotlite и Kotgres генераторы SQL и JDBC кода на Kotlin для Sqlite и Postgresql

01.04.2021 22:22:46 | Автор: admin

Eсть таблица:

CREATE TABLE person(    id         uuid primary key,    name       text,    birth_date date)

и соотвтетствующий ей дата-класс:

data class Person(    val id: UUID,    val name: String,    val birthDate: LocalDate,)

Что если для того чтобы выполнить базовые CRUD операции:

  • сохранить список Person-ов

  • вычитать всё из таблицы

  • удалить все записи в таблице

  • найти по ID

  • удалить по имени

будет достаточно создать интерфейс:

@SqliteRepositoryinterface PersonRepository : Repository<People> {    fun saveAll(people: List<Person>)    fun selectAll(): List<Person>    fun deleteAll()    fun selectBy(id: UUID): Person?    fun deleteBy(name: String)}

а имплеметнация будет сгенерирована автоматически.

Напоминает Spring Data? Но это не Spring, не Hibernate и даже не JPA.

TL;DR

  • Kotlin-центричная библиотека (не фреймворк)

  • Не ORM (не содержит JPA)

  • Генерирует SQL и JDBC до этапа компиляции (Kotlin Annotation Precessing)

  • Нет магии в рантайме

  • Сгенерированный код отформатирован, можно дебажить, работает навигация, можноскопировать в проект и модифицировать

  • Удобный DSL для работы с базой

  • Есть 2 имплементации: под Postgres и Sqlite

Конфигурация

На данный момент есть 2 реализации этой библиотеки: для Postgresql и Sqlite. В данной статье примеры будут для Sqlite.

Для начала нужно сконфигурировать Gradle (да простят меня пользователи Maven):

build.gradle.kts

plugins {    kotlin("kapt") version "1.4.31" //(1)    kotlin("plugin.serialization") version "1.4.31"}dependencies {    implementation("com.github.mfarsikov:kotlite-core:0.5.0") //(2)    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0") //(3)    implementation("org.xerial:sqlite-jdbc:3.34.0") //(4)    kapt("com.github.mfarsikov:kotlite-kapt:0.5.0") //(5)    }kapt {    arguments {        arg("kotlite.db.qualifiedName", "my.pkg.DB") //(6)    }}
Пояснения по build.gradle.kts
  1. Добавить плагин для обработки аннотаций и генерации кода (`kapt`).

  2. Добавить зависимость на core-часть библиотеки. Она содержит необходимые аннотации, и некоторый обвязочный код.

  3. Сериализация в/из JSON используется для вложенных коллекций.

  4. Непосредственно драйвер Sqlite базы.

  5. Плагин создаст kapt конфигурацию, в которую нужно включить зависимость на `kapt`-часть библиотеки. Именно она занимается генерацией SQL запросов и кода JDBC.

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

Репозиторий

import kotlite.annotations.SqliteRepository@SqliteRepositoryinterface PersonRepository

От такого репозитория пользы немного, но уже для него Kotlite может сгенерировать имплементацию.

Команда ./gradlew kaptKotlin сгенерирует:

build/generated/source/kapt/PersonRepositoryImpl.kt
@Generatedinternal class PersonRepositoryImpl(    private val connection: Connection) : PersonRepository

Первый запрос

import kotlite.annotations.Queryimport kotlite.annotations.SqliteRepository@SqliteRepositoryinterface PersonRepository {    @Query("SELECT id, name, birth_date FROM person")    fun findPeople(): List<Person>}

Kotlite знает достаточно, чтобы сгенерировать код для этого запроса:

  • Из возвращаемого типа List следует, что записей может быть от 0 до N

  • Из возвращаемого типа Person следует, что каждый кортеж будет содержать три поля: id, name и birth_date.

  • По конвенции, для поля в классе birthDate ожидается значение в кортеже birth_date

В результате сгенерируется метод:

build/generated/source/kapt/PersonRepositoryImpl.kt
public override fun findPeople(): List<Person> {    val query = "SELECT id, name, birth_date FROM person"    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = it.getObject("birth_date", LocalDate::class.java),                        id = it.getObject("id", java.util.UUID::class.java),                        name = it.getString("name"),                    )            }            acc        }    }}

Как выполнить этот запрос?

В конфигурации (build.gradle.kts) мы указывали, что нужно сгенерировать класс my.pkg.DB. Это главный объект, через который осуществляется доступ ко всем сгенерированным репозиториям. Для его создания нужен DataSource. Все объявленные нами репозитории доступны внутри транзакции:

main.kt

import my.pkg.DBimport org.sqlite.SQLiteDataSourcefun main() {    val datasource = SQLiteDataSource().apply {        url = "jdbc:sqlite:path/to/my/test.db"    }    val db = DB(datasource)    val people: List<Person> = db.transaction {        personRepository.findPeople()    }    println(people)}

Запрос с параметрами

@Query("SELECT id, name, birth_date FROM person WHERE name = :firstName")fun findPeopleBy(firstName: String): List<Person>

Параметры метода могут быть использованы в запросе. Перед именем параметра должно быть двоеточие.

сгенерированный метод
public override fun findPeopleBy(firstName: String): List<Person> {    val query = "SELECT id, name, birth_date FROM person WHERE name = ?"    return connection.prepareStatement(query).use {        it.setString(1, firstName)        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )            }            acc        }    }}

Возвращаемые типы

В зависимости от возвращаемого типа Kotlite генерирует различное поведение.

Список (List)

Cамый обычный тип. Полностью соответствует тому, что возвращает база от 0 до N элементов. Другие коллекции не поддерживаются.

Сущность (Entity)

На первый взгляд ничего особенного, но есть несколько нюансов:

  • что если запрос не вернет ни одного значения

  • что если запрос вернет больше одного значения

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

@Query("SELECT id, name, birth_date FROM person WHERE name = :name")fun findPersonBy(name: String): Person
Сгенерированный код
public override fun findPersonBy(name: String): Person {    val query = """     |SELECT id, name, birth_date FROM person WHERE name = ?     |LIMIT 2     """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, name)        it.executeQuery().use {            if (it.next()) {                val result =                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                throw NoSuchElementException()            }        }    }}

Для выбора первого значения можно пометить метод аннотацией kotlite.annotations.First

Скаляр

Возвращаемым типом может быть не только сущность, но и любое скалярное ("примитивное") значение. Например: Int, String, UUID LocalDateи т.п.

@Query("SELECT name FROM person WHERE id = :id")fun findPersonNameBy(id: UUID): String

Если запрос не вернул ни одного значения, или если вернул больше одного, то так-же как и для сущности будет выброшено исключение.

Сгенерированный метод
public override fun findPersonNameBy(id: UUID): String {    val query = """        |SELECT name FROM person WHERE id = ?        |LIMIT 2        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, id)        it.executeQuery().use {            if (it.next()) {                val result =                    it.getString(1)                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                throw NoSuchElementException()            }        }    }}

Для выбора первого значения можно пометить метод аннотацией kotlite.annotations.First

Nullable значения

Скаляр или сущность могут быть объявлены как Nullable. В таком случае вернется nullесли запрос не вернул ни одной записи.

@Query("SELECT name FROM person WHERE id = :id")fun findPersonNameBy(id: UUID): String?
Сгенерированный метод
public override fun findPersonNameBy(id: UUID): String? {    val query = """     |SELECT name FROM person WHERE id = ?     |LIMIT 2     """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, id)        it.executeQuery().use {            if (it.next()) {                val result =                    it.getString(1)                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                null            }        }    }}

Постраничный вывод (Pagination)

Pageableопределяет сколько элементов размещается на странице, и какую страницу нужно выбрать

import kotlite.aux.page.Pageimport kotlite.aux.page.Pageable@SqliteRepositoryinterface PersonRepository : Repository<Person> {    @Query("SELECT name FROM person")    fun selectAll(pageable: Pageable): Page<String>}
Сгенерированный метод
public override fun selectAll(pageable: Pageable): Page<String> {    val query = """        |SELECT name FROM person        |LIMIT ? OFFSET ?        """.trimMargin()    return connection.prepareStatement(query).use {        it.setInt(1, pageable.pageSize)        it.setInt(2, pageable.offset)        it.executeQuery().use {            val acc = mutableListOf<String>()            while (it.next()) {                acc +=                    it.getString(1)            }            Page(pageable, acc)        }    }}

Генерация SQL

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

Для этого нужно дать библиотеке немного информации о том, с какой сущностью мы работаем. Делается это через переменную типа, интерфейса kotlite.aux.Repository

import kotlite.annotations.SqliteRepositoryimport kotlite.aux.Repository@SqliteRepositoryinterface PersonRepository : Repository<Person> 

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

Известно название таблицы. По конвенции это имя клaсса, сконвертированное из UpperCamelCaseв snake_case. Название таблицы может быть явно указано в аннотацииkotlite.annotations.Table.

Также известно количество, названия и типы колонок таблицы. Названия колонок конвертируются из camelCaseв snake_case Альтернативно, название может быть указано в аннотации kotlite.annotations.Column

Что это нам дает?

Сохранение и обновление

Для любого метода, имя которого начинается на save(либо который помечен аннотацией kotlite.annotations.Save) будет сгенерирован INSERT . Такой метод должен принимать в качестве параметро либо саму сущность, либо список сущностей. Возвращаемый тип должен быть Unit

fun save(person: Person)
Сгенерированный метод
public override fun save(person: Person): Unit {    val query = """        |INSERT INTO person        |("birth_date", "id", "name")        |VALUES (?, ?, ?)        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.executeUpdate()    }}

Если сущность имеет первичный ключ (как минимум одно из полей помечено аннотацией kotlite.annotations.ID) будет сгенерирован INSERT/UPDATE

Сгенерированный метод
public override fun save(person: Person): Unit {    val query = """    |INSERT INTO person    |("birth_date", "id", "name")    |VALUES (?, ?, ?)    |ON CONFLICT (id) DO     |UPDATE SET "birth_date" = EXCLUDED."birth_date", "id" = EXCLUDED."id", "name" = EXCLUDED."name"    |""".trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.executeUpdate()    }}

Это поведение можно переопределить аннотацией:

import kotlite.annotations.OnConflictFail@OnConflictFailfun save(person: Person)

Оптимистическая блокировка

Если числовое поле класса помечено аннотацией kotlite.annotations.Versionдля такой сущности запросы обновления и удаления будут содержать проверку текущей версии

Сгенерированные методы
public override fun save(person: Person): Unit {    val query = """        |INSERT INTO person        |("birth_date", "id", "name", "version")        |VALUES (?, ?, ?, ? + 1)        |ON CONFLICT (id) DO         |UPDATE SET "birth_date" = EXCLUDED."birth_date", "id" = EXCLUDED."id", "name" = EXCLUDED."name", "version" = EXCLUDED."version"        |WHERE person.version = EXCLUDED.version - 1        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.setInt(4, person.version)        val rows = it.executeUpdate()        if (rows != 1) {            throw OptimisticLockFailException()        }    }}public override fun delete(person: Person): Unit {    val query = """        |DELETE         |FROM person        |WHERE "id" = ? AND "version" = ?        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.id)        it.setInt(2, person.version)        val rows = it.executeUpdate()        if (rows != 1) {            throw OptimisticLockFailException()        }    }}

Сге

Удаление

Для любого метода, имя которого начинается на delete (или который помечен аннотацией kotlite.annotations.Delete) будет сгенерирован DELETE

fun deleteAll()
Сгенерированный метод
public override fun deleteAll(): Unit {    val query = """    |DELETE     |FROM person    """.trimMargin()    return connection.prepareStatement(query).use {        it.executeUpdate()    }}

Такой метод может принимать сущность в качестве параметра:

fun delete(person: Person)

Удаление будет происходить по всем полям сущности

Сгенерированный метод
public override fun delete(person: Person): Unit {    val query = """        |DELETE         |FROM person        |WHERE "birth_date" = ? AND "id" = ? AND "name" = ?        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.executeUpdate()    }}

Если сущность имеет первичный ключ (хотя бы одно поле помечено kotlite.annotations.Id) удаление будет по первичному ключу:

Сгенерированный метод
public override fun delete(person: Person): Unit {    val query = """    |DELETE     |FROM person    |WHERE "id" = ?    """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.id)        it.executeUpdate()    }}

Кроме этого метод удаления может так-же принимать и другие параметры, см. разделы "Метод с параметрами" и "Сложные условия" ниже.

Метод без параметров

Любой метод, объявленный в репозитории, считается запросом типа SELECT(кроме методов, названия которых начинаются со слов saveи delete).

fun selectAll(): List<Person>
Сгенерированный метод
public override fun selectAll(): List<Person> {    val query = """     |SELECT "birth_date", "id", "name"     |FROM person     """.trimMargin()    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )            }            acc        }    }}

Функции fun selectAll(): List<Person>и fun blaBlaBla(): List<Person> ничем не отличаются друг от друга и для них будет сгенерирован абсолютно одинаковый код.

Метод с параметрами

Все параметры метода должны совпадать по названию с полями класса. Они будут использованы как условия равенства во WHEREи объединены через AND.

fun selectBy(name: String, birthDate: LocalDate): Person?
Сгенерированный метод
public override fun selectBy(name: String, birthDate: LocalDate): Person? {    val query = """     |SELECT "birth_date", "id", "name"     |FROM person     |WHERE "name" = ? AND "birth_date" = ?     |LIMIT 2     """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, name)        it.setObject(2, birthDate)        it.executeQuery().use {            if (it.next()) {                val result =                    Person(                        birthDate = java.time.LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                null            }        }    }}

Сложные условия

Если вместо стандартного равенства нужно использовать >, <=, != и т.д., или условия должны быть объединены с помощьюOR с расстановкой скобок, для этого подойдет аннотация kotlite.annotations.Where:

@Where("name = :name OR birth_date < :birthDate")fun selectBy(name: String, birthDate: LocalDate): Person?

Её содержимое будет подставлено в запрос почти без изменений.

Сгенерированный метод
public override fun selectBy(name: String, birthDate: LocalDate): Person? {    val query = """        |SELECT "birth_date", "id", "name"        |FROM person        |WHERE name = ? OR birth_date < ?        |LIMIT 2        """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, name)        it.setObject(2, birthDate)        it.executeQuery().use {            if (it.next()) {                val result =                    Person(                        birthDate = java.time.LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                null            }        }    }}

Сортировка

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

@OrderBy("name DESC, birth_date")fun selectAll(): List<Person>
Сгенерированный метод
public override fun selectAll(): List<Person> {    val query = """    |SELECT "birth_date", "id", "name"    |FROM person    |ORDER BY name DESC, birth_date    """.trimMargin()    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )            }            acc        }    }}

Вложенные объекты

Вложенные объекты не могут быть представлены как связь один-к-одному. Поля вложенных объектов должны быть представлены колонками в этой же таблице. Т.е. быть @Embeddableв терминах JPA.

data class Person(    val name: Name,)data class Name(    val firstName: String,    val lastName: String,)
CREATE TABLE person(    first_name text,    last_name text)

Альтернативно вложенные объекты могут быть сериализованы в JSON. Предмет для добавления в ближайшие версии.

Вложенные коллекции

Вложенные коллекции не могут быть представлены как связь один-ко-многим.Вместо этого они автоматически сериализуются в JSON.

data class Person(    val habits: List<String>)@SqliteRepositoryinterface PersonRepository: Repository<Person> {    fun save(person: Person)    fun select(): List<Person>}
Сгенерированные методы
public override fun select(): List<Person> {    val query = """    |SELECT "habits"    |FROM person    """.trimMargin()    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        habits = Json.decodeFromString(it.getString("habits")),                    )            }            acc        }    }}public override fun save(person: Person): Unit {    val query = """    |INSERT INTO person    |("habits")    |VALUES (?)    """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, Json.encodeToString(person.habits))        it.executeUpdate()    }}

Особенности (сравнительно с JPA/Hibernate)

  • Из-за использования SQL, рефакторинг (например, переименование поля сущности) может потребовать изменения тех запросов, которые были написаны вручную.

  • Поскольку во главу угла поставлена простота, нет возможности создавать связи `один-к-одному`, `один-ко-многим` (и нет N+1 проблемы).

  • Нет ленивых загрузок (и нет `SessionClosedException`).

  • Нет встроенного механизма конвертеров типов (не переусложнен API, библиотека решает только одну задачу).

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

  • Не питает иллюзий относительно легкой миграции на другую базу данных.

На этом наши полномочия всё

Спасибо за уделенное внимание.

Sqlite

Posgresql

Подробнее..
Категории: Kotlin , Postgresql , Sql , Sqlite , Generator , Jdbc , Kapt

Перевод Папа может в си, или Простая и аккуратная кодогенерация для SQLite

02.11.2020 20:06:40 | Автор: admin
image

Совсем скоро, 4 и 5 ноября, у нас стартуют новые потоки курсов SQL для анализа данных и C++ разработчик, специально к их старту мы подготовили этот перевод из блога Facebook Engineering с обзором полезного инструмента.

SQLite используется широко, но написание хорошо протестированных и поддерживаемых уровней доступа к данным в лучшем случае может стать сложной задачей. Многие команды применяют некую генерацию кода, чтобы избежать необходимости менять десятки порядковых номеров каждый раз, когда добавляется столбец, но такой подход приводит к ошибкам. Компилятор CQL в CG/SQL позволяет создавать сложные хранимые процедуры с большими запросами, а с помощью комбинаций синтаксических хелперов и сильной типизации эти процедуры гораздо проще получить и сохранить. Сочетание сильной типизации в языке и средства для хорошего юнит-тестирования может дать уверенность в том, что даже очень сложная логика корректна.



Что это такое


CG/SQL это система генерации кода для популярной библиотеки SQLite, позволяющая разработчикам писать хранимые процедуры в варианте Transact-SQL (T-SQL) и компилировать их в код на языке C, использующий для выполнения операций C API SQLite. CG/SQL позволяет инженерам создавать сложные хранимые процедуры с большими запросами без ручной проверки кода, необходимой существующим методам.

Вся система также включает функции управления и обновления схемы, создания тестирующего кода для хранимых процедур, получения планов запросов для процедур, а также взаимодействия с хранимыми процедурами из других языков, например, Java и Objective-C. Вывод в JSON позволяет делать разбор или интерфейсный код. Пакет содержит обширную документацию по языку и системе.

Что делает инструмент


Компилятор CQL делает большую часть грязной работы. Он читает схему и процедуры, предоставляя язык с сильной типизацией и сотнями ошибок компиляции, разработанных для предотвращения проблем SQLite, возникающих во время выполнения. Компилятор тщательно отслеживает типы данных переменных и типы схем, сообщая о несоответствиях: например о попытке присвоить обнуляемые столбцы необнуляемым выходным переменным, а в противном случае гарантирует, что API SQLite используются последовательно и корректно.

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

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

Зачем это нужно?


SQLite используется широко, но написание хорошо протестированных и поддерживаемых уровней доступа к данным в лучшем случае может стать сложной задачей. Многие команды применяют некую генерацию кода, чтобы избежать необходимости менять десятки порядковых номеров каждый раз, когда добавляется столбец, но такой подход приводит к ошибкам. Компилятор CQL в CG/SQL позволяет создавать сложные хранимые процедуры с большими запросами, а с помощью комбинаций синтаксических хелперов и сильной типизации эти процедуры гораздо проще получить и сохранить. Сочетание сильной типизации в языке и средства для хорошего юнит-тестирования может дать уверенность в том, что даже очень сложная логика корректна. Синтаксические хелперы преобразуют безопасный код в канонический SQL, так инженеры пишут меньше кода, но при этом код корректнее и он выполняется везде. Посмотрим на пример:

create procedure insert_a_row(like your_table)begin  insert into your_table from arguments;end;

Этот код создает процедуру вставки в любую таблицу (здесь это your_table), аргументы которой именно столбцы таблицы. Вы не забудете о столбцах, не поставите десятки аргументов в неправильном порядке. Конструкции краткие, кроме того, они устойчивы к ошибкам, что позволяет инженерам проще генерировать код без необходимости проверять каждый его кусочек вручную.

CG/SQL на Github

CG/SQL конечно полезная вещь, но скидочный промокод HABR не менее полезен, он даст вам получить дополнительные 10% к скидке указанной на баннере.

image




Рекомендуемые статьи


Подробнее..

Перевод SQLite с использованием Go и Python

10.02.2021 20:14:57 | Автор: admin

Для будущих студентов курса "Golang Developer. Professional" и всех интересующихся подготовили перевод интересного материала.

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


Введение

В основном я предпочитаю использовать реляционные базы данных (SQL), поскольку они предоставляют несколько возможностей, которые весьма полезны при работе с данными. SQLite - отличный выбор, так как база данных там представляет собой единый файл, что упрощает обмен данными. Несмотря на то, что это единый файл, SQLite может обрабатывать до 281 терабайта данных. SQLite также поставляется с клиентом командной строки sqlite3, который отлично подходит для быстрого прототипирования.

Примечание: В других базах данных есть транзакции, языки запросов и схемы. Однако базы данных на основе SQL имеют тенденцию быть более развитыми испытанными временем, а сам SQL стандартизирован.

Определения

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

SQLite: это встроенная база данных SQL. SQLite небольшая, быстрая и самая часто используемая база данных. Это также, безусловно, мой излюбленный способ обмена большими объемами данных между двумя программами.

Транзакции: вы вводите данные в базу данных SQL внутри транзакции. Это означает, что либо поступают сразу все данные, либо не поступают никакие. Транзакции на порядки упрощают код повторного выполнения операций в конвейерах данных.

Схема: данные в реляционных базах данных имеют схему, что означает, что их легче проверить на достоверность.

SQL: Structured Query Language (язык структурированных запросов) это язык для выборки и изменения данных. Вам не нужно изобретать еще один способ выборки интересующих вас частей данных. SQL это устоявшийся формат, вокруг которого накоплено много знаний и инструментов.

Проект

Мы напишем HTTP-сервер на Go, который будет получать уведомления о сделках (trades) и хранить их в базе данных SQLite. Затем мы напишем программу на Python, которая будет обрабатывать эти данные.

В Go мы будем использовать github.com/mattn/go-sqlite3, который является оболочкой для библиотеки C SQLite.

Примечание: Поскольку go-sqlite использует cgo, изначальное время сборки будет больше обычного. Использование cgo означает, что результирующий исполняемый файл зависит от некоторых разделяемых библиотек из ОС, что немного усложняет распространение.

В Python мы будем использовать встроенный модуль sqlite3 и функцию read_sql Pandas, чтобы загружать данные. Исходный код из этой статьи доступен по ссылке.

Код на Go

Код, который я собираюсь вам показать, можно найти в файле trades.go.

Листинг 1: Структура Trade

37 // Trade - это сделка о покупке/продаже по символу ценной бумаги.38 type Trade struct {39     Time   time.Time40     Symbol string41     Price  float6442     IsBuy  bool43 }

В Листинге 1 показана структура данных Trade. У нее есть поле Time, отражающее время сделки, поле Symbol, хранящее биржевое сокращение (символ акции, например, AAPL), поле Price, содержащее цену, и логический флаг, который сообщает, это сделка покупки или продажи.

Листинг 2: Схема базы данных

24     schemaSQL = `25 CREATE TABLE IF NOT EXISTS trades (26     time TIMESTAMP,27     symbol VARCHAR(32),28     price FLOAT,29     buy BOOLEAN30 );31 32 CREATE INDEX IF NOT EXISTS trades_time ON trades(time);33 CREATE INDEX IF NOT EXISTS trades_symbol ON trades(symbol);34 `

В Листинге 2 объявляется схема базы данных, соответствующая структуре Trade. В строке 25 мы создаем таблицу под названием trades. В строках 26-29 мы определяем столбцы таблицы, которые соответствуют полям структуры Trade. В строках 32-33 мы создаем индексы для таблицы, чтобы обеспечить быстрое запрашивание по time и symbol.

Листинг 3: Внесение записи

16     insertSQL = `17 INSERT INTO trades (18     time, symbol, price, buy19 ) VALUES (20     ?, ?, ?, ?21 )22 `

Листинг 3 определяет SQL-запрос для внесения записи в базу данных. В строке 20 мы используем символ-заполнитель ? для параметров этого запроса. Никогда не используйте fmt.Sprintf для создания SQL-запроса вы рискуете создать SQL-инъекцию.

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

Листинг 4: DB

45 // DB - это база данных биржевых торгов.46 type DB struct {47     sql    *sql.DB48     stmt   *sql.Stmt49     buffer []Trade50 }

В Листинге 4 описана структура DB. В строке 47 мы устанавливаем соединение с базой данных. В строке 48 мы храним подготовленный (предварительно скомпилированный) оператор для вставки, а в строке 49 находится наш буфер ожидающих транзакций в памяти.

Листинг 5: NewDB

53 // NewDB создает Trades для управления торговлей акциями в базе данных SQLite. Этот API не является потокобезопасным.54 func NewDB(dbFile string) (*DB, error) {55     sqlDB, err := sql.Open("sqlite3", dbFile)56     if err != nil {57         return nil, err58     }59 60     if _, err = sqlDB.Exec(schemaSQL); err != nil {61         return nil, err62     }63 64     stmt, err := sqlDB.Prepare(insertSQL)65     if err != nil {66         return nil, err67     }68 69     db := DB{70         sql:    sqlDB,71         stmt:   stmt,72         buffer: make([]Trade, 0, 1024),73     }74     return &db, nil75 }

Листинг 5 демонстрирует создание готовой к использованию базы данных DB. В строке 55 мы подключаемся к базе данных с помощью драйвера sqlite3. В строке 60 мы применяем SQL-схему, чтобы создать таблицу trades, если она еще не существует. В строке 64 мы предварительно компилируем инструкцию InsertSQL. В строке 72 мы создаем внутренний буфер с длиной 0 и емкостью 1024.

Примечание: Чтобы не усложнять код, предоставляемый мной API DB не горутино-безопасен (в отличие от sql.DB). Если несколько горутин вызывают API одновременно, вы столкнетесь с состоянием гонки. Я оставлю это вам в качестве упражнения сделайте этот код горутино-безопасным.

Листинг 6: Add

77 // Add сохраняет сделку в буфер. Как только буфер заполняется, сделки вносятся в базу данных.79 func (db *DB) Add(trade Trade) error {80     if len(db.buffer) == cap(db.buffer) {81         return errors.New("trades buffer is full")82     }83 84     db.buffer = append(db.buffer, trade)85     if len(db.buffer) == cap(db.buffer) {86         if err := db.Flush(); err != nil {87             return fmt.Errorf("unable to flush trades: %w", err)88         }89     }90 91     return nil92 }

В Листинге 6 приведен метод Add. В строке 84 мы добавляем сделку (trade) в буфер в памяти. В строке 85 мы проверяем, заполнен ли буфер, и если да, то мы вызываем метод Flush в строке 86, который вносит записи из буфера в базу данных.

Листинг 7: Flush

94  // Flush вносит ждущие обработки сделки в базу данных.95  func (db *DB) Flush() error {96      tx, err := db.sql.Begin()97      if err != nil {98          return err99      }100 101     for _, trade := range db.buffer {102         _, err := tx.Stmt(db.stmt).Exec(trade.Time, trade.Symbol, trade.Price, trade.IsBuy)103         if err != nil {104             tx.Rollback()105             return err106         }107     }108 109     db.buffer = db.buffer[:0]110     return tx.Commit()111 }

В Листинге 7 приведен метод Flush. В строке 96 мы начинаем транзакцию. В строке 101 мы итерируем по внутреннему буферу, а в строке 102 вносим каждую сделку. Если при внесении произошла ошибка, мы выполняем rollback в строке 104. В строке 109 мы сбрасываем буфер сделок в памяти. И наконец, в строке 110 мы выполняем commit.

Листинг 8: Close

113 // Close вносит (посредством Flush) все сделки в базу данных и предотвращает любую торговлю в будущем.114 func (db *DB) Close() error {115     defer func() {116         db.stmt.Close()117         db.sql.Close()118     }()119 120     if err := db.Flush(); err != nil {121         return err122     }123 124     return nil125 }

В Листинге 8 приведен метод Close. В строке 120 мы вызываем Flush, чтобы внести все оставшиеся сделки в базу данных. В строках 116 и 117 мы закрываем (close) инструкцию и базу данных. Функции, создающие DB, должны иметь функцию defer db.Close(), чтобы убедиться, что связь с базой данных закончена.

Листинг 9: Импорты

5 // Ваши пакеты main и test требуют эти импорты, чтобы пакет sql был правильно инициализирован.6 // _ "github.com/mattn/go-sqlite3"7 8 import (9     "database/sql"10     "errors"11     "fmt"12     "time"13 )

В Листинге 9 приведен импорт для файла. В строке 5 мы импортируем database/sql, которая определяет API для работы с базами данных SQL. database/sql не содержит какого-либо конкретного драйвера базы данных.

Как говорится в комментарии к оператору импорта, чтобы использовать пакет trades, вам необходимо импортировать пакет, который реализует драйвер базы данных sqlite3 (например, github.com/mattn/go-sqlite3). Поскольку вы импортируете пакет, реализующий драйвер только для небольшого изменения регистрации протокола sqlite3, мы используем перед импортом, сообщая компилятору Go, что то, что мы не используем этот пакет в коде это нормально.

Листинг 10: Пример использования

Код этих примеров можно найти в файле tradestest.go.

66 func ExampleDB() {67     dbFile := "/tmp/db-test" + time.Now().Format(time.RFC3339)68     db, err := trades.NewDB(dbFile)69     if err != nil {70         fmt.Println("ERROR: create -", err)71         return72     }73     defer db.Close()74 75     const count = 10_00076     for i := 0; i < count; i++ {77         trade := trades.Trade{78             Time:   time.Now(),79             Symbol: "AAPL",80             Price:  rand.Float64() * 200,81             IsBuy:  i%2 == 0,82         }83         if err := db.Add(trade); err != nil {84             fmt.Println("ERROR: insert - ", err)85             return86         }87     }88 89     fmt.Printf("inserted %d records\n", count)90     // Вывод:91     // inserted 10000 records92 }

В Листинге 10 показан пример использования (в виде тестируемого примера). В строке 67 мы создаем новую базу данных, а в строке 73 мы закрываем ее с помощью оператора defer. В строке 76 мы запускаем цикл для вставки сделок, а в строке 83 мы собственно и вставляем сделку в базу данных.

Код на Python

Примеры кода на Python можно найти в файле analysis_trades.py.

Листинг 11: Импорты

02 import sqlite303 from contextlib import closing04 from datetime import datetime05 06 import pandas as pd

В Листинге 11 показаны библиотеки, которые мы используем в нашем Python-коде. В строке 2 мы импортируем встроенный модуль sqlite3, а в строке 6 библиотеку pandas.

Листинг 12: Select SQL

08 select_sql = """09 SELECT * FROM trades10 WHERE time >= ? AND time <= ?11 """

В Листинге 12 показан SQL-запрос для получения данных. В строке 10 мы выбираем все столбцы из таблицы trades. В строке 10 мы добавляем элемент WHERE для выбора временного диапазона. Как и в Go-коде мы используем заполнители аргументов ? и не пишем SQL-запросы вручную.

Листинг 13: Загрузка сделок

14 def load_trades(db_file, start_time, end_time):15     """Загружаем сделки из db_file за заданный временной диапазон."""16     conn = sqlite3.connect(db_file)17     with closing(conn) as db:18         df = pd.read_sql(select_sql, db, params=(start_time, end_time))19 20     # Мы не можем использовать здесь detect_types=sqlite3.PARSE_DECLTYPES, поскольку Go вставляет часовой пояс, а sqlite3 Python не умеет обрабатывать его.22     # Смотрите https://bugs.python.org/issue29099# См. Https://bugs.python.org/issue2909923     df["time"] = pd.to_datetime(df["time"])24     return df

В Листинге 13 показан код для загрузки сделок из заданного временного диапазона. В строке 16 мы подключаемся к базе данных. В строке 17 мы используем менеджер контекста, что-то вроде defer в Go, чтобы убедиться, что база данных закрыта. В строке 18 мы используем функцию pandas read_sql для загрузки данных из SQL-запроса в DataFrame. Python имеет API для подключения к базам данных (например, database/sql), а Pandas может использовать любой совместимый драйвер. В строке 23 мы конвертируем столбец time в Timestamp pandas. Это особенность SQLite, в котором нет встроенной поддержки TIMESTAMP типов.

Листинг 14: Средняя цена

27 def average_price(df):28     """Возвращает среднюю цену в df, сгруппированную по (stock, buy)"""29     return df.groupby(["symbol", "buy"], as_index=False)["price"].mean()

В Листинге 14 показано, как вычислить среднюю цену на symbol и buy. В строке 29 мы используем DataFrame groupby для группировки по symbol и buy. Мы используем as_index=False, чтобы получить symbol и buy в виде столбцов в итоговом фрейме данных. Затем мы берем столбец price и вычисляем среднее значение для каждой группы.

Листинг 15: Вывод

symbol,buy,priceAAPL,0,250.82925665004535AAPL,1,248.28277375538832GOOG,0,250.11537993385295GOOG,1,252.4726772487683MSFT,0,250.9214212695317MSFT,1,248.60187022941685NVDA,0,250.3844763417279NVDA,1,249.3578146208962

В Листинге 15 показан результат выполнения кода Python на тестовых данных.

Заключение

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

Я максимально упростил код, чтобы показать наиболее интересные моменты. Есть несколько мест, где вы можете попробовать улучшить его:

  • Добавить код повторного выполнения в Flush

  • Выполнить дополнительную проверку ошибок в Close

  • Сделать DB горутино-безопасным

  • Реализовать больше аналитики в Python-части

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

Если вы не хотите напрямую работать с SQL, есть несколько альтернатив, в мире Go есть такие пакеты, как sqlx, gorm, а в мире Python есть SQLAlchemy, который может использовать Pandas.

Приятного вам кодинга. Дайте мне знать, как вы используете SQL в целом и SQLite в частности.


Узнать подробнее о курсе "Golang Developer. Professional".

Смотреть открытый вебинар по теме Go-каналы снаружи и внутри.

Подробнее..

Чтобы первый блин не вышел комом. Советы начинающему разработчику сервиса

26.05.2021 10:15:20 | Автор: admin

Добрый день, уважаемые читатели! Материал адресован всем специалистам, работающим с данными, которые решили написать первое веб-приложение. В данной публикации я не буду выкладывать листинги кода. На просторах Интернета есть масса практических примеров сборки сервисов, написанных на разных фреймворках. Но вот теоретических статей о логике процесса, архитектуре решения, а, главное, трудностях, с которыми впервые столкнется специалист, крайне мало. Я решил заполнить эту нишу и описать свой личный опыт, который кому-то может быть полезен.

Специально для статьи я подготовил два идентичных примера на Flask и Dash и выложил их на GitHub. В них иллюстрируется расчет и вывод показателей юнит-экономики абстрактного IT-маркета, который называется Хабр (а почему бы и нет, ведь сейчас все компании начали заниматься электронной коммерцией:).

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

ОПП: не умеешь не берись! Когда речь заходит об ОПП, мне почему-то автоматически вспоминается Django с его классами. Но если посмотреть работы начинающих data scientist-ов или аналитиков данных, то мы увидим совсем другую картину. Классы применяются ради самих классов. В данную структуру языка просто сливается весь код. За что отвечает этот монстр? За все! Как искать ошибки или переписывать код, не понятно. Лично у меня такое мнение на этот счет. Если не знаешь когда, как и почему следует применять ОПП, то лучше для небольших разработок использовать процедурно-функциональный стиль.

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

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

Муки выбора или о разных фреймворках замолвим слово. Сочетание каких технологий можно использовать для создания собственного сервиса? Приведу несколько вариантов, которые сразу приходят на ум. Заранее прошу прощения, что обойду вниманием PHP, Ruby, C#:

  • Flask статичные страницы с шаблонами HTML+CSS

  • Django статичные страницы с шаблонами HTML+CSS

  • Flask Rest API/FastAPI/Django Rest Framework динамические страницы HTML+CSS+фреймворк Javascript (Vue, React, Angular)

  • Dash (по сути работает Flask) Dask (по сути работает React)

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

  • Нужно выводить таблицы, графики, интерактивные элементы здесь и сейчас Dash

  • Нужно рендерить отдельные показатели на статичной странице. Есть время на эксперименты с дизайном, но нет помощи фронтенд-разработчика Flask

  • Нужно выводить разноплановую информацию, нужна интерактивность. Есть много времени, есть ресурсы, плюс поддержка верстальщика и фронтенд-программиста FastAPI Vue.js

Теперь приведу скриншоты работ на Flask и Dash и сделаю несколько замечаний касательно данных платформ.

Задача состояла в том, что нужно было рассчитать, а потом отобразить 6 таблиц с показателями юнит-экономики, то есть сформировать веб-дашборд. Сразу скажу, что на разработку примеров я потратил примерно одинаковое время. Кардинального различия в результатах я не увидел, но есть нюансы.

В проекте Flask файл, который отвечает за вывод результатов, страницы html и фреймворк css это разные сущности. Документация по Bootstrap4 довольно качественная, но так как у меня нет навыков верстки, мне не удалось добиться корректного вывода всех сводных таблиц.

В проекте Dash за все операции отвечает единый файл, так как я выбрал вариант с хранением таблицы стилей в app.py. Если дашборд простой, то читаемость кода будет приемлемой. Но с ростом проекта с этим могут возникнуть трудности. Стили можно переместить в папку asset. Можно ли как-то еще раздробить основной файл я не знаю. Сразу из коробки имеется хорошая поддержка всех аналитических компонентов, включая таблицы, но нужно время для ознакомления со спецификой разработки.

Архитектура всему голова. Заранее продумывайте архитектуру своего приложения. Все файлы должны быть разнесены по модулям согласно их функционалу. При этом нужно стремиться к тому, чтобы, если изъять из сервиса часть модулей, остальная часть программы сохранила работоспособность. Компоненты должны спокойно интегрироваться в другой сервис с минимальными доработками. Переходим к моим ошибкам. Скрипты для запуска etl-процессов и расчета показателей лежат рядом с главным файлом проекта.

Многофункциональности здесь не место. В продолжение предыдущего пункта. Ваше приложение должно делать хорошо что-то одно. Мой сервис выполняет etl-команды, формирует БД, а затем наполняет ее записями и отвечает за вывод дашборда. Это три разных процесса, которые с большой долей вероятности в реальности будут разнесены во времени. И конечно, нужно убирать файл с данными из приложения, так как он только занимает место.

Что SQL-запросом вытянешь, то и считать будешь. Максимально перенесите расчетную нагрузку на сторону БД. При этом следует учитывать разности в диалектах sql. Старайтесь писать запросы максимально универсальными. Мои ошибки. База данных в качестве физического файла присутствует в проекте. В запросах имеются уникальные конструкции диалекта SQLite.

Pandas мне друг, но производительность дороже. Мне пришлось применить данную библиотеку, так требовалась именно сводная таблица, а получить ее на стороне БД проблематично. В большинстве случаев лучше обойтись только нативным Python.

Не все то золото, что YAML-файл. Идею применения yaml файла для хранения констант проекта я почерпнул из одного видео-ролика практикующего data scientist-а на Youtube. Что в этом плохого или хорошего я не знаю. Решать только вам.

А не замахнуться ли нам на Docker. Небольшое лирическое отступление. Чего мне реально не хватает в Windows, так это Docker. В Windows 10 эту проблему решили, а вот в предыдущих версиях пользователям остается лишь устанавливать Docker Toolbox. Но в настоящее время разработка и поддержка данного продукта завершена, хотя архивный файл можно по-прежнему скачать на официальном аккаунте Docker на GitHub. Лично у меня по некоторым причинам установлен Windows 8.1, поэтому я задался вопросом, как еще можно заполучить в распоряжение эту программу. Установку второй операционной системы я отмел сразу, а вот вариант с виртуальной машиной меня заинтересовал. Для экономии ресурсов я выбрал Debian 10. Если выделить под нужды ВМ один процессор и три гигабайта оперативной памяти, то вполне можно тестировать свои идеи. Но стоит оговориться, что если захочется собрать и запустить контейнер с Apache Airflow, то указанных вычислительных мощностей будет недостаточно.

Теперь можно возвращаться к нашим приложениям. Как сбилдить и запустить контейнер я рассказывать не буду, так как данную информацию легко можно нагуглить в Интернете. Есть лишь пара моментов, на которых я заострю внимание. В процессе сборки будет выдаваться предупреждение о необходимости создания виртуального окружения внутри контейнера. Я решил пренебречь им, так как контейнер и так изолирован от рабочей среды Linux. И еще момент. После того, как приложение на Dash было упаковано в docker-контейнер, перестал отображаться логотип Хабра. Явной причины этого я быстро не нашел, а время, отведенное на эксперимент, было исчерпано.

Семь раз проверь, один раз задеплой. Завершить публикацию я решил на банальной ноте. А именно напомнить вам, о том, как важно проверять результаты перед сдачей. Пара досадных опечаток в комментариях, конечно, не поставят крест на всем проекте, но ведь может сложиться ситуация, что приложение просто не запуститься на демонстрации.

И вот вам конкретный пример. Я построил контейнер на Dash, а дашборд в браузере не отображается. В локальном варианте все было нормально. Оказалось, я просто забыл поменять в файле app.py хост с 127.0.0.1, на 0.0.0.0.

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

На этом все. Всем здоровья, удачи и профессиональных успехов!

Подробнее..

Визуализация sqlite в node.js

17.11.2020 10:12:56 | Автор: admin


Довольно часто на этапе прототипирования (и всегда в пет-проектах) я не запариваюсь с бэкэндом и поднимаю апи на express с sqlite3. Это легко и довольно удобно для несложной логики, а для сложной есть бэкэндеры с их отдельным миром. Единственный геморрой, который долгое время меня преследовал на этапе написания апи невозможность быстро заглянуть в базу и отследить изменения. Можно дебажить по памяти, конечно, но это как-то странно, когда можно просто вытащить содержимое по запросу и отобразить его на какой-нибудь страничке. Вот только каждый раз заново парсить json и распихивать его в таблицы по лучшим практикам очередного фреймворка мне не хотелось, поэтому я после недолгих поисков нашёл sqljs и набросал на нём простейший визуализатор.


Про sqljs


Это библиотека, позволяющая создавать sqlite базы, читать, писать и вообще строить любой апи на них. Он построен на wasm, поэтому работает медленнее чем тот же sqlite3, и использует emscripten для сборки. По умолчанию создаётся in-memory база, но можно читать из файла и экспортировать файл из памяти. Демо. В конце концов, sqljs не завязан на node или любое другое окружение и спокойно работает в браузере, но мне было проще запустить его из ноды, всё равно ж использую.

Ссылки
Вот сайт, документация и примеры. Есть легаси-версия с asm.js и web worker версия, все дистрибутивы доступны через CDN и на GitHub.

Использование


0. Подключаем библиотеку

CDN:
  <script src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/sql.js/1.4.0/dist/sql-wasm.js" integrity="sha512-8oJoeo0ykAzuJzQFJDnwz9t4Rr+1xue7LFX+kr0NJMpOHH9QJPC563If+sakheUe3QbLwTTgXIGPC6YZTwp7Iw==" crossorigin="anonymous"></script>

npm:
  npm install sqljs


1. Инициализация библиотеки
  // Используя модуль:  const initSqlJs = require('sql.js');  // При использовании в браузере:  var initSqlJs = window.initSqlJs;


2. Загружаем инстанс
  const SQL = await initSqlJs({    // Асинхронная загрузка бинарника wasm. Разумеется, его можно хранить и у себя    // В node не требуется    locateFile: file => `https://sql.js.org/dist/${file}`  });


3. Создаём базу, выполняем INSERT/SELECT
  var db = new SQL.Database();  // Также можно использовать new SQL.Database(data), где  // data это Uint8Array с файлом sqlite  sqlstr = "CREATE TABLE hello (a int, b char);";  sqlstr += "INSERT INTO hello VALUES (0, 'hello');"  sqlstr += "INSERT INTO hello VALUES (1, 'world');"  db.run(sqlstr);  var res = db.exec("SELECT * FROM hello");  /*  [    {columns:['a','b'], values:[[0,'hello'],[1,'world']]}  ]  */


4. Формируем смешанный запрос и привязываем переменные к значениям ответа
  var stmt = db.prepare("SELECT * FROM hello WHERE a=:aval AND b=:bval");  var result = stmt.getAsObject({':aval' : 1, ':bval' : 'world'});  console.log(result); // {a:1, b:'world'}  // Освобождаем память выражения  stmt.free();  // После освобождения использовать выражение нельзя,  // но бесконтрольное использование приведёт к утечкам памяти


Демо: https://jsfiddle.net/5f3ahx8o/

Визуализация


Из-за разницы в производительности и не особенно нужного in-memory, возможно и стоило изначально брать проверенный sqlite3, но надо было бы притянуть немного бойлерплейта, да и логично было бы пустить отображение через апи, а мне хотелось иметь отдельно работающий молчаливый сервис. Чтобы не плодить сущности я взял express-handlebars для отображения и прокинул в основную вьюху {{{renderedTables}}}.

Базу забирал из той же директории, но можно прописать любой путь.
  var filebuffer = fs.readFileSync('sqlitedb');  /* ... */  initSqlJs().then(function(SQL){    var db = new SQL.Database(filebuffer);


Данные из всех таблиц получаются в две строчки:
  var schema = db.exec('SELECT name, sql FROM sqlite_master WHERE type="table";')[0].values;  schema.forEach(t => all[t[0]] = db.exec('SELECT * FROM ' + t[0]));


Дальше идёт скучный рендер таблицы и, наконец, отправка её на страницу. Полный код есть в репо.

Заключение


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



На правах рекламы


Мощные виртуальные серверы с процессорами AMD EPYC для разработчиков. Частота ядра CPU до 3.4 GHz. Максимальная конфигурация позволит оторваться на полную 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe.

Подробнее..

Первые шаги в BI-аналитике. Роль Data Engineering

01.05.2021 10:11:55 | Автор: admin

Добрый день, уважаемые читатели! Материал носит теоретический характер и адресован исключительно начинающим аналитикам, которые впервые столкнулись с BI-аналитикой.

Что традиционно понимается под этим понятием? Если говорить простым языком, то это комплексная система (как и, например, бюджетирование) по сбору, обработке и анализу данных, представляющая конечные результаты в виде графиков, диаграмм, таблиц.

Это требует слаженной работы сразу нескольких специалистов. Дата-инженер отвечает за хранилища и ETL/ELT-процессы, аналитик данных помогает в заполнении базы данных, аналитик BI разрабатывает управленческие панели, бизнес-аналитик упрощает коммуникации с заказчиками отчетов. Но такой вариант возможен, только если фирма готова оплачивать работу команды. В большинстве случаев небольшие компании для минимизации затрат делают ставку на одного человека, который зачастую вообще не обладает широким кругозором в области BI, а имеет лишь шапочное знакомство с платформой для отчетов.

В таком случае происходит следующее: сбор, обработка и анализ данных происходит силами единственного инструмента самой BI-платформой. При этом данные предварительно никак не очищаются, не проходят компоновки. Забор информации идет из первичных источников без участия промежуточного хранилища. Результаты такого подхода можно легко лицезреть на тематических форумах. Если постараться обобщить все вопросы касательно BI-инструментов, то в топ-3 попадут, наверное, следующие: как загрузить в систему плохо структурированные данные, как по ним рассчитать требуемые метрики, что делать, если отчет работает очень медленно. Что удивительно, на этих форумах вы практически не найдете обсуждений ETL-инструментов, описания опыта применения хранилищ данных, лучших практик программирования и запросов SQL. Более того, я неоднократно сталкивался с тем, что опытные BI-аналитики не очень лестно отзывались о применении R/Python/Scala, мотивируя это тем, что все проблемы можно решить только силами BI-платформы. Вместе с тем всем понятно, что грамотный дата инжиниринг позволяет закрывать массу проблем при построении BI-отчетности.

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

Data BI Самый простой вариант. Именно с него начинается прототипирование управленческих панелей. В роли источника данных часто выступает отдельный (-ые) статичный файл (csv, txt, xlsx и т. д.).

Плюсы. Самый быстрый способ построения отчетности. Идеально подходит, для ситуационной аналитики или когда результат нужен был еще вчера. Не требует применения вспомогательных инструментов, следовательно, не нужно тратить ресурсы на их поддержание. Аналитик BI не обязан иметь компетенции в области дата инжиниринга или программирования.

Минусы. Далеко не изо всех источников можно забрать информацию напрямую (пример, прикладные решения на платформе 1С). Если массивы плохо структурированы, то это потребует много дополнительных шагов по их обработке. Качество данных никак не проверяется (проблема дубликатов, пустых строк, некорректного написания значений и т. д.). При большом количестве строк заметно замедляется работа самой BI-платформы, вплоть до полной невозможности перестраивать графики и диаграммы. Нет возможности составить расписание на обновление исходников.

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

Плюсы. Есть возможность агрегировать разрозненные, однотипные файлы. Нагрузку по хранению информации теперь несет хранилище. Есть возможность задействовать всю мощь языка запросов SQL (или его диалекта), чтобы отфильтровать или агрегировать сырые строки перед их передачей в BI-инструмент. Уменьшается размер файла с управленческими панелями.

Минусы. Нет контроля над первичными данными, поэтому в хранилище заливается большое количество ненужной информации. Качество загружаемых датасетов никак не контролируется. Добавление данных в базу осуществляется в ручном режиме. Аналитик должен на базовом уровне знать SQL.

Data ETL DB BI Частичная автоматизация. В качестве ETL-инструмента может выступать как программный продукт с графическим интерфейсом, так и код написанный на R/Python/Scala и т. д. Все данные проходят предварительный предпроцессинг. Структура наполняемых таблиц прописывается заранее.

Плюсы. Возможность загружать только хорошо структурированную информацию, которая прошла предварительную верификацию. Экономия места в базе данных. Снижается количество доработок на BI-платформе.

Минусы. Аналитик должен уверенно владеть ETL-инструментом и языком запросов SQL. Процесс разработки и тестирования скриптов требует времени. Если источников информации много, то затрудняется синхронизация получения информации.

Для иллюстрации этого варианта я решил написать простейшие скрипты. В рамках игрушечного примера я использую SQLite. Это позволит прикрепить базу данных к публикации, чтобы каждый желающий мог попрактиковаться в написании скриптов (архив). Датасет для разбора это E-Commerce Data с сайта Kaggle.

# импорт библиотекimport pandas as pd# опции отображенияpd.set_option('display.max_columns', 10)pd.set_option('display.expand_frame_repr', False)path_dataset = 'dataset/ecommerce_data.csv'# Предварительная обработка датасетаdef func_main(path_dataset: str):    # Считываем датасет    df = pd.read_csv(path_dataset, sep=',')    # Приводим названия столбцов датасета к нижнему регистру    list_col = list(map(str.lower, df.columns))    df.columns = list_col    # Избавляемся от времени и трансформируем строку-дату в правильный формат    df['invoicedate'] = df['invoicedate'].apply(lambda x: x.split(' ')[0])    df['invoicedate'] = pd.to_datetime(df['invoicedate'], format='%m/%d/%Y')    # Рассчитываем сумму покупки по каждому товару    df['amount'] = df['quantity'] * df['unitprice']    # Удаляем ненужные для дальнейшего анализа столбцы    df_result = df.drop(['invoiceno', 'quantity', 'unitprice', 'customerid'], axis=1)    # Задаем порядок вывода столбцов для визуального контроля результата    df_result = df_result[['invoicedate', 'country', 'stockcode', 'description', 'amount']]    return df_result# Таблица Продажиdef func_sale():    tbl = func_main(path_dataset)    df_sale = tbl.groupby(['invoicedate', 'country', 'stockcode'])['amount'].sum().reset_index()    return df_sale# Таблица Страныdef func_country():    tbl = func_main(path_dataset)    df_country = pd.DataFrame(sorted(pd.unique(tbl['country'])), columns=['country'])    return df_country# Таблица Товарыdef func_product():    tbl = func_main(path_dataset)    df_product = tbl[['stockcode','description']].\        drop_duplicates(subset=['stockcode'], keep='first').reset_index(drop=True)    return df_product

В коде сочетается Extract и Transform. Считываем датасет, парсим столбец с датами. Рассчитываем сумму покупки по каждой строке и удаляем ненужные для дальнейшего анализа колонки. Так как датафрейм записывается в базу данных не монолитом, а разбивается на таблицы, то готовим три вспомогательные функции.

# импорт библиотекimport pandas as pdimport sqlite3 as sqfrom etl1 import func_country,func_product,func_salecon = sq.connect('sale.db')cur = con.cursor()## Таблица Страны# cur.executescript('''DROP TABLE IF EXISTS country;#                     CREATE TABLE IF NOT EXISTS country (#                     country_id INTEGER PRIMARY KEY AUTOINCREMENT,#                     country TEXT NOT NULL UNIQUE);''')# func_country().to_sql('country',con,index=False,if_exists='append')## Таблица Товары# cur.executescript('''DROP TABLE IF EXISTS product;#                     CREATE TABLE IF NOT EXISTS product (#                     product_id INTEGER PRIMARY KEY AUTOINCREMENT,#                     stockcode TEXT NOT NULL UNIQUE,#                     description TEXT);''')# func_product().to_sql('product',con,index=False,if_exists='append')## Таблица Продажи (основная)# cur.executescript('''DROP TABLE IF EXISTS sale;#                     CREATE TABLE IF NOT EXISTS sale (#                     sale_id INTEGER PRIMARY KEY AUTOINCREMENT,#                     invoicedate TEXT NOT NULL,#                     country_id INTEGER NOT NULL,#                     product_id INTEGER NOT NULL,#                     amount REAL NOT NULL,#                     FOREIGN KEY(country_id)  REFERENCES country(country_id),#                     FOREIGN KEY(product_id)  REFERENCES product(product_id));''')## Таблица Продажи (временная)# cur.executescript('''DROP TABLE IF EXISTS sale_data_lake;#                     CREATE TABLE IF NOT EXISTS sale_data_lake (#                     sale_id INTEGER PRIMARY KEY AUTOINCREMENT,#                     invoicedate TEXT NOT NULL,#                     country TEXT NOT NULL,#                     stockcode TEXT NOT NULL,#                     amount REAL NOT NULL);''')# func_sale().to_sql('sale_data_lake',con,index=False,if_exists='append')## Перегружаем данные из вспомогательной таблицы (sale_data_lake) в основную (sale)# cur.executescript('''INSERT INTO sale (invoicedate, country_id, product_id, amount)#                     SELECT  sdl.invoicedate, c.country_id, pr.product_id, sdl.amount#                     FROM sale_data_lake as sdl LEFT JOIN country as c ON sdl.country = c.country#                     LEFT JOIN product as pr ON sdl.stockcode = pr.stockcode#                     ''')## Очищаем вспомогательную таблицу# cur.executescript('''DELETE FROM sale_data_lake''')def select(sql):  return pd.read_sql(sql,con)sql = '''select *        from (select s.invoicedate,                      c.country,                      pr.description,                      round(s.amount,1) as amount               from sale as s left join country as c on s.country_id = c.country_id                            left join product as pr on s.product_id = pr.product_id)'''print(select(sql))cur.close()con.close()

На следующем этапе (Load) мы создаем четыре таблицы. Две из них будут справочниками. Одна содержать сгруппированную информацию по продажам. Нам также потребуется вспомогательная таблица, в которую мы запишем строки с продажами до момента замены текстовых значений на числовые ид. На последнем шаге очистим ее от всех значений.

В заключении нам остается лишь выполнить тестовый запрос SQL, чтобы проверить корректность всех операций. Если все сделано правильно, запускаем BI-платформу.

Так как BI-инструмент не может из коробки напрямую подключиться к SQLite напишем простейший скрипт на Python.

import pandas as pdimport sqlite3 as sqcon = sq.connect('C:/Users/Pavel/PycharmProjects/test/sale.db')cur = con.cursor()def select(sql):  return pd.read_sql(sql,con)sql = '''select *        from (select s.invoicedate,                      c.country,                      pr.description,                      replace(round(s.amount,1),'.',',') as amount               from sale as s left join country as c on s.country_id = c.country_id                            left join product as pr on s.product_id = pr.product_id)'''tbl = select(sql)print(tbl)

После загрузки данных в систему и проверки корректности распознанных форматов можно приступать к непосредственному построению дашборда.

Data Workflow management platform + ETL DB BI Полная автоматизация. Оркестратор скриптов берет на себя контроль за своевременным выполнением всех вспомогательных процессов в системе.

Плюсы. Возможность оптимально настроить время сбора данных из разрозненных источников. Можно мониторить ошибки и перезапускать упавшие задачи.

Минусы. Усложнение инфраструктуры. Рост требований к квалификации аналитика BI. Необходимо осваивать дополнительные инструменты или языки программирования.

Data Workflow management platform + ELT Data Lake Workflow management platform + ETL DB BI Самый сложный вариант, где информация проходит двухступенчатый конвейер: сначала это неструктурированные данные (Data Lake), а затем уже хранилище (DB), где информация отсортирована и преобразована к требуемому виду.

Плюсы. Возможность разнести во времени сбор информации и ее обработку. Если на этапе проектирования таблиц учтены не все требования, возможно обращение за дополнительными данными в Data Lake.

Минусы. Аналогичны предыдущему варианту. Если выбранная платформа Data Lake платная, как следствие рост расходов на аналитику компании.

Краткие выводы.

  • Построение BI-аналитики без даты инжиниринга возможно лишь на старте.

  • Если аналитик BI работает в единственном числе и система постоянно усложняется, то он обязан подменять собой сразу несколько специалистов.

  • Понимание базовых принципов построения хранилищ данных, уверенное владение SQL, программирование на каком-либо языке и, конечно, дизайнерские навыки вот далеко не полный перечень требований к сотруднику, которому делегируется проектировать управленческие панели.

На этом все. Всем здоровья, удачи и профессиональных успехов!

Подробнее..

Как проанализировать рынок фотостудий с помощью Python (23). База данных

31.07.2020 10:05:30 | Автор: admin
В предыдущей статье в рамках коммерческого проекта по анализу рынка фотостудий рассмотрел создание парсинга: выгрузка списка фотостудий, списка залов, данных по бронированию с момента открытия зала до последней брони.

Полученную информацию в кэше хранить нецелесообразно, необходимо использовать базу данных.

В статье рассмотрю:
  • создание простой SQLite базы данных;
  • запись информации с помощь Python;
  • чтение данных и перевод в формат DataFrame;
  • обновление парсинга с учетом данных БД.




Требования к базе данных


Основное требование к БД по проекту: хранить данные и иметь возможность их оперативно извлечь.

Нашей БД не требуется:
  • разграничивать доступ к схемам, т.к. доступ будет только у пользователя парсингом;
  • сохранять доступ 24/7, т.к. извлечение данных допустимо по мере необходимости проведения анализа;
  • создание процедур, т.к. все вычисления будут проводится в python.

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

Особенности работы с SQLite через python


Для работы с SQLite через python используем библиотеку sqlite3.
Подключаемся к базе данных простой командой:
sqlite3.connect(путь к файлу)

Если файл отсутствует, будет создана новая база.

Запросы к базе выполняются следующим образом:
conn = sqlite3.connect(путь к файлу)cur = conn.cursor()cur.execute(запрос)df = cur.fetchall()

cur.fetchall() выполняется в том случае, когда в результате запроса мы хотим получить данные из БД.

В конце записи данных в БД не забывайте заканчивать транзакцию:
conn.commit()

а в конце работы с базой не забывайте её закрывать:
conn.close()

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

Создание таблиц стандартное:
CREATE TABLE t1 (поле1 тип, поле2 тип...)

или более универсальный вариант, создающий таблицу в случае её отсутствия:
CREATE TABLE IF NOT EXISTS t1 (поле1 тип, поле2 тип...)

Записываем данные в таблицу, избегая повторов:
INSERT OR IGNORE INTO t1 (поле1, поле2, ...) VALUES(значение1, значение2, ...)

Обновляем данные:
UPDATE t1 SET поле1 = значение1 WHERE поле2 = значение2

Для более удобной работы с SQLite можно использовать SQLite Manager или DB Browser for SQLite.

Первая программа является расширением к браузеру и выглядит как чередование строки запроса и блока ответа:


Вторая программа полноценное desktop-приложение:




Структура базы данных


БД будет состоять из 4 таблиц: студии, залы, 2 таблицы бронирования.
В выгружаемых данных по бронированию есть информация о будущих периодах, которая с новым парсингом может измениться. Затирать данные нежелательно (их можно использовать, например, для вычисления дня/часа, когда была сделана бронь). Поэтому, одна таблица бронирования необходима для сырых данных парсинга, вторая для последних, актуальных.

Создаем таблицы:
def create_tables(conn, table = 'all'):    cur = conn.cursor()        if (table == 'all') or (table == 'uStudios'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uStudios            (studio_id INT PRIMARY KEY UNIQUE,            name TEXT UNIQUE,            metro TEXT,            address TEXT,            phone TEXT,            email TEXT,            established_date DATE)            ''')        print('Table uStudios is created.')    if (table == 'all') or (table == 'uHalls'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uHalls            (hall_id INT PRIMARY KEY UNIQUE,            studio_id INT,            name TEXT,            is_hall INT,            square FLOAT,            ceiling FLOAT,            open_date DATE)            ''')        print('Table uHalls is created.')    if (table == 'all') or (table == 'uBooking_parsing'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uBooking_parsing            (hall_id INT,            date DATE,            hour INT,            is_working_hour INT,            min_hours INT,            price INTEGER,            is_booked INT,            duration INT,            parsing_date DATE)            ''')        print ('Table uBooking_parsing is created.')    if (table == 'all') or (table == 'uBooking'):        cur.execute('''            CREATE TABLE IF NOT EXISTS uBooking            (hall_id INT,            date DATE,            hour INT,            is_working_hour INT,            min_hours INT,            price INTEGER,            is_booked INT,            duration INT,            parsing_date DATE)            ''')        print ('Table uBooking is created.')


Параметром table задаю название таблицы, которую необходимо создать. По умолчанию создает все.

В полях таблиц видны данные, которые не парсили (дата открытия студии, дата открытия зала). Вычисление этих полей опишу позже.

Взаимодействие с базой данных


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


1. Запись списка фотостудий в базу данных


На входе в процедуру передаем параметры соединения с БД и таблицу в виде DataFrame. Записываем данные построчно, перебирая все строчки циклом. Полезным для этой операции свойством строковых данных в python является замена символов "?" элементами кортежа, указанным после.
Процедура записи списка фотостудий выглядит следующим образом:
def studios_to_db(conn, studio_list):     cur = conn.cursor()    for i in studio_list.index:        cur.execute('INSERT OR IGNORE INTO uStudios (studio_id, name, metro, address, phone, email) VALUES(?, ?, ?, ?, ?, ?)',                   (i,                   studio_list.loc[i, 'name'],                   studio_list.loc[i, 'metro'],                   studio_list.loc[i, 'address'],                   studio_list.loc[i, 'phone'],                   studio_list.loc[i, 'email']))



2. Выгрузка списка фотостудий из базы данных


На вход в процедуру передаем параметры соединения с БД. Выполняем select-запрос, перехватываем выгружаемые данные и записываем в DataFrame. Переводим дату основания фотостудии в формат даты.
Полностью процедура выглядит следующим образом:
def db_to_studios(conn):    cur = conn.cursor()    cur.execute('SELECT * FROM uStudios')    studios = pd.DataFrame(cur.fetchall()                           , columns=['studio_id', 'name', 'metro', 'address', 'phone', 'email', 'established_date']                          ).set_index('studio_id')    studios['established_date'] = pd.to_datetime(studios['established_date'])    return studios



3. Запись списка залов в базу данных


Процедура аналогична записи списка фотостудий: передаем параметры подключения и таблицу залов, построчно записываем данные в базу.
Процедура записи списка залов в БД
def halls_to_db(conn, halls):     cur = conn.cursor()    for i in halls.index:        cur.execute('INSERT OR IGNORE INTO uHalls (hall_id, studio_id, name, is_hall, square, ceiling) VALUES(?, ?, ?, ?, ?, ?)',                   (i,                   halls.loc[i, 'studio_id'],                   halls.loc[i, 'name'],                   halls.loc[i, 'is_hall'],                   halls.loc[i, 'square'],                   halls.loc[i, 'ceiling']))



4. Выгрузка списка залов из базы данных


Процедура аналогична выгрузки списка фотостудий: передача параметров подключения, select-запрос, перехват, запись в DataFrame, перевод даты открытия зала в формат даты.
Единственное отличие: id студии и признак зала записались в байтовом виде. Возвращаем значение функцией:
int.from_bytes(число, 'little')

Процедура выгрузки списка залов выглядит следующим образом:
def db_to_halls(conn):    cur = conn.cursor()    cur.execute('SELECT * FROM uHalls')    halls = pd.DataFrame(cur.fetchall(), columns=['hall_id', 'studio_id', 'name', 'is_hall', 'square', 'ceiling', 'open_date']).set_index('hall_id')    for i in halls.index:        halls.loc[i, 'studio_id'] = int.from_bytes(halls.loc[i, 'studio_id'], 'little')        halls.loc[i, 'is_hall'] = int.from_bytes(halls.loc[i, 'is_hall'], 'little')    halls['open_date'] = pd.to_datetime(halls['open_date'])    return halls



5. Выгрузка информации по бронированию из базы данных


В процедуру передаем параметры подключения к БД и параметр parsing, показывающий из какой таблицы по бронированию запрашиваем информацию: 0 из актуальной (по умолчанию), 1 из таблицы парсинга. Далее выполняем select-запрос, перехватываем его, переводим в DataFrame. Даты переводим в формат дат, числа из байтового формата в формат чисел.
Процедура выгрузки информации по бронированию:
def db_to_booking(conn, parsing = 0):    cur = conn.cursor()    if parsing == 1:        cur.execute('SELECT * FROM uBooking_parsing')    else:        cur.execute('SELECT * FROM uBooking')    booking = pd.DataFrame(cur.fetchall(), columns=['hall_id',                                                      'date', 'hour',                                                      'is_working_hour',                                                      'min_hours',                                                      'price',                                                      'is_booked',                                                      'duration',                                                      'parsing_date'])    booking['hall_id'] = [int.from_bytes(x, 'little') if not isinstance(x, int) else x for x in booking['hall_id']]    booking['is_booked'] = [int.from_bytes(x, 'little') if not isinstance(x, int) else x for x in booking['is_booked']]    booking['date'] = pd.DataFrame(booking['date'])    booking['parsing_date'] = pd.DataFrame(booking['parsing_date'])        return booking



6. Запись информации по бронированию в базу данных


Самая сложная функция взаимодействия с БД, т.к. она инициирует парсинг данных по бронированию. На входе передаем процедуре параметры подключения к базе данных и список id залов, который необходимо актуализировать.

Чтобы определить последнюю дату актуальных данных,
запросим из базы последнюю дату парсинга по каждому id зала:
parsing_date = db_to_booking(conn, parsing = 1).groupby('hall_id').agg(np.max)['parsing_date']


Перебираем каждый id зала с помощью цикла.

В каждом id зала первом делом определяем
количество недель, которые необходимо парсить в прошлом:
        try:            last_day_str = parsing_date[id]            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            delta_days = (datetime.datetime.now() - last_day).days            weeks_ago = delta_days // 7        except:            last_day_str = '2010-01-01'            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            weeks_ago = 500


Если id зала есть в базе, то вычисляем. Если нет, то парсим 500 недель в прошлое или останавливаемся, когда 2 месяца не было броней (ограничение описано в предыдущей статье).

Дальше выполняем процедуры парсинга:
        d = get_past_booking(id, weeks_ago = weeks_ago)                d.update(get_future_booking(id))        book = hall_booking(d)


Вначале парсим информацию по бронированию из прошлого до актуальных данных, потом из будущего (до 2 месяцев, когда записей не было) и в конце переводим данные из формата json в DataFrame.

Завершающим этапом записываем данные по бронированию зала в базу и закрываем транзакцию.
Процедура записи информации по бронированию в базу данных выглядит следующим образом:
def booking_to_db(conn, halls_id):    cur = conn.cursor()    cur_date = pd.Timestamp(datetime.date.today())    parsing_date = db_to_booking(conn, parsing = 1).groupby('hall_id').agg(np.max)['parsing_date']        for id in halls_id:                #download last parsing_date from DataBase        try:            last_day_str = parsing_date[id]            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            delta_days = (datetime.datetime.now() - last_day).days            weeks_ago = delta_days // 7        except:            last_day_str = '2010-01-01'            last_day = datetime.datetime.strptime(last_day_str, '%Y-%m-%d')            weeks_ago = 500                d = get_past_booking(id, weeks_ago = weeks_ago)                d.update(get_future_booking(id))        book = hall_booking(d)        for i in list(range(len(book))):#book.index:            cur.execute('INSERT OR IGNORE INTO uBooking_parsing (hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date) VALUES(?,?,?,?,?,?,?,?,?)',                       (book.iloc[i]['hall_id'],                       book.iloc[i]['date'].date().isoformat(),                       book.iloc[i]['hour'],                       book.iloc[i]['is_working_hour'],                       book.iloc[i]['min_hours'],                       book.iloc[i]['price'],                       book.iloc[i]['is_booked'],                       book.iloc[i]['duration'],                       cur_date.date().isoformat()))        conn.commit()        print('hall_id ' + str(id) + ' added. ' + str(list(halls_id).index(id) + 1) + ' from ' + str(len(halls_id)))



Обновление дней открытия студии и залов


Дата открытия зала это самая ранняя дата бронирования зала.
Дата открытия фотостудии это самая ранняя дата открытия зала этой студии.

Исходя из этой логики,
выгружаем самые ранние даты бронирования каждого зала из базы
halls = db_to_booking(conn).groupby('hall_id').agg(min)['date']


Затем построчно обновляем данные открытия залов:
    for i in list(range(len(halls))):        cur.execute('''UPDATE uHalls SET open_date = '{1}' WHERE hall_id = {0}'''                    .format(halls.index[i], str(halls.iloc[i])))


Данные открытия фотостудий обновляем аналогично: выгружаем данные по датам открытия залов из базы данных, вычисляем наименьшую дату для каждой студии, переписываем дату открытия фотостудии.
Процедура обновления дат открытия:
def update_open_dates(conn):        cur = conn.cursor()        #update open date in uHalls    halls = db_to_booking(conn).groupby('hall_id').agg(min)['date']        for i in list(range(len(halls))):        cur.execute('''UPDATE uHalls SET open_date = '{1}' WHERE hall_id = {0}'''                    .format(halls.index[i], str(halls.iloc[i])))    #update open date in uStudios    studios = db_to_halls(conn)    studios['open_date'] = pd.to_datetime(studios['open_date'])    studios = studios.groupby('studio_id').agg(min)['open_date']    for i in list(range(len(studios))):        cur.execute('''UPDATE uStudios SET established_date = '{1}' WHERE  studio_id = {0}'''                    .format(studios.index[i], str(studios.iloc[i])))        conn.commit()



Обновление парсинга


Все процедуры в этой и предыдущей статьях мы объединим в данной процедуре. Её можно запускать как при первом парсинге, так и при обновлении данных.
Процедура выглядит следующим образом:
def update_parsing(directory = './/', is_manual = 0):        start_time = time.time()        #is DataBase exists?    if not os.path.exists(directory + 'photostudios_moscow1.sqlite'):        if is_manual == 1:            print('Data base is not exists. Do you want to create DataBase (y/n)? ')            answer = input().lower()        else:             answer == 'y'                if answer == 'y':            conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')            conn.close()            print('DataBase is created')        elif answer != 'n':            print('Error in input!')            return list()        print('DataBase is exists')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()            #connect to DataBase    conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')    cur = conn.cursor()           #has DataBase 4 tables?    tables = [x[0] for x in list(cur.execute('SELECT name FROM sqlite_master WHERE type="table"'))]    if not ('uStudios' in tables) & ('uHalls' in tables) & ('uBooking_parsing' in tables) & ('uBooking' in tables):        if is_manual == 1:            print('Do you want to create missing tables (y/n)? ')            answer = input().lower()        else:            answer = 'y'                if anwer == 'y':            if not ('uStudios' in tables):                create_tables(conn, table = 'uStudios')            if not ('uHalls' in tables):                create_tables(conn, table = 'uHalls')            if not ('uBooking_parsing' in tables):                create_tables(conn, table = 'uBooking_parsing')            if not ('uBooking' in tables):                create_tables(conn, table = 'uBooking')        elif answer != 'n':            print('Error in input!')            return list()    conn.commit()    print(str(tables) + ' are exist in DataBase')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        #update uStudios    studios = studio_list()    new_studios = studios[[x not in list(db_to_studios(conn).index) for x in list(studios.index)]]    if len(new_studios) > 0:        print(str(len(new_studios)) + ' new studios detected: \n' + str(list(new_studios['name'])))        studios_to_db(conn, new_studios)        conn.commit()    print('Studio list update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        #update uHalls    halls = hall_list(list(studios.index)).sort_index()    new_halls = halls[[x not in list(db_to_halls(conn).index) for x in list(halls.index)]]    if len(new_halls) > 0:        halls_to_db(conn, new_halls)        conn.commit()    print('Halls list update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()            #update uBooking_parsing    booking_to_db(conn, halls.index)           conn.commit()    print('Booking_parsing update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        #update uBooking from uBooking_parsing    cur.execute('DELETE FROM uBooking')    cur.execute('''        insert into uBooking (hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date)         select hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date        from        (            select *, row_number() over(partition by hall_id, date, hour order by parsing_date desc) rn             from uBooking_parsing        ) t        where rn = 1    ''')    conn.commit()    print('Booking update was successful')    print("--- %s seconds ---" % (time.time() - start_time))    start_time = time.time()        update_open_dates(conn)    conn.commit()    print('Open date update was successful')    print("--- %s seconds ---" % (time.time() - start_time))        conn.close()


Разберем ее работу по порядку.

На входе в процедуру передаем 2 параметра: адрес папки, откуда брать базу данных или куда ее установить (по умолчанию берем папку с python-документов), и необязательный параметр is_manual, который при значении 1 будет запрашивать необходимость создания базы данных или таблиц в случае их отсутствия.

Вначале проверяем есть ли база данных. Если нет, создаём:
    if not os.path.exists(directory + 'photostudios_moscow1.sqlite'):        if is_manual == 1:            print('Data base is not exists. Do you want to create DataBase (y/n)? ')            answer = input().lower()        else:             answer == 'y'                if answer == 'y':            conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')            conn.close()            print('DataBase is created')        elif answer != 'n':            print('Error in input!')            return list()


Присоединяемся к БД и сохраняем функцию курсок отдельной переменной:
    conn = sqlite3.connect(directory + 'photostudios_moscow1.sqlite')    cur = conn.cursor() 


Следующим действием проверяем, все ли таблицы созданы. Если нет, создаем недостающие. В конце изменений завершаем транзакцию:
    tables = [x[0] for x in list(cur.execute('SELECT name FROM sqlite_master WHERE type="table"'))]    if not ('uStudios' in tables) & ('uHalls' in tables) & ('uBooking_parsing' in tables) & ('uBooking' in tables):        if is_manual == 1:            print('Do you want to create missing tables (y/n)? ')            answer = input().lower()        else:            answer = 'y'                if anwer == 'y':            if not ('uStudios' in tables):                create_tables(conn, table = 'uStudios')            if not ('uHalls' in tables):                create_tables(conn, table = 'uHalls')            if not ('uBooking_parsing' in tables):                create_tables(conn, table = 'uBooking_parsing')            if not ('uBooking' in tables):                create_tables(conn, table = 'uBooking')        elif answer != 'n':            print('Error in input!')            return list()    conn.commit()



Обновляем список фотостудий. Сравниваем с данными БД и выводим количество и список новых фотостудий:
    studios = studio_list()    new_studios = studios[[x not in list(db_to_studios(conn).index) for x in list(studios.index)]]    if len(new_studios) > 0:        print(str(len(new_studios)) + ' new studios detected: \n' + str(list(new_studios['name'])))        studios_to_db(conn, new_studios)


conn.commit()


Обновляем список залов и выводим название новых:
    halls = hall_list(list(studios.index)).sort_index()    new_halls = halls[[x not in list(db_to_halls(conn).index) for x in list(halls.index)]]    if len(new_halls) > 0:        halls_to_db(conn, new_halls)        conn.commit()



Обновляем информацию по бронированию в таблице uBooking_parsing. Скрипт получился простым, т.к. всю сложную работу мы сделали в самой процедуре booking_to_db
    booking_to_db(conn, halls.index)           conn.commit()



Обновляем актуальную информацию по бронированию в таблице uBooking. Для этого удаляем старую версию uBooking и записываем данные из таблицы uBooking_parsing с последними (для каждого зала, даты и часа брони) датами парсинга:
    cur.execute('DELETE FROM uBooking')    cur.execute('''        insert into uBooking (hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date)         select hall_id, date, hour, is_working_hour, min_hours, price, is_booked, duration, parsing_date        from        (            select *, row_number() over(partition by hall_id, date, hour order by parsing_date desc) rn             from uBooking_parsing        ) t        where rn = 1    ''')    conn.commit()


Обновляем даты открытия студий и залов:
    update_open_dates(conn)    conn.commit()


И закрываем базу
    conn.close()



Парсинг с сохранением данных в БД настроен успешно!

Инициируем парсинг/обновление следующей процедурой:
update_parsing()



Итог


В данной и предыдущей статьях мы рассмотрели алгоритм работы парсинга открытой информацией по фотостудиям. Полученные данные собрали в базу данных.

В следующей статье рассмотрим примеры анализа полученных данных.

Готовый проект вы можете найти на моей странице в github.
Подробнее..

Из песочницы Бот Telegram для пиццерии на Python с помощью telebot

23.06.2020 12:07:45 | Автор: admin

Вступление


На Хабре уже есть статья о Telegram боте, написанном на Python с помощью telebot. Признаться, свое знакомство с чат-ботами в недавно разблокированном мессенджере я начинал с этой статьи. Моя писанина это дополнение, включающее в себя работу с Inline кнопками и базой данных.


Установка библиотеки


Telebot библиотека для взаимодействия с Telegram API, которая привлекла меня простотой, поэтому я считаю, что для новичков она подходит на все 100%.


Чтобы установить библиотеку, введите в терминале следующую команду.


pip install pytelegrambotapi


Получение API-токена


Чтобы создать бота, обратимся к "отцу" всех ботов @BotFather. Отправьте команду /newbot, после чего введите имя бота, затем алиас, оканчивающийся на bot.


image

Пишем код


Лучше записывать API-токены, номера кошелька и тому подобное в отдельный файл, поэтому создадим файл config.py:


TOKEN = "1176585885:AAH-RA2kZym9E5tR8JFLtYYjNxrMHnsJr0o"

Основной код находится в bot.py


# импорт библиотекиimport telebot# Подтягиваем токен бота и все необходимое для получение средств за пиццуfrom config import TOKEN# Создание ботаbot = telebot.TeleBot(TOKEN)# Декоратор для обработки всех текстовых сообщений@bot.message_handler(content_types=['text'])def all_messages(msg):    # Получаем сообщение пользователя    message = msg.text    # Получаем Telegram id пользователя (очевидно, для каждого пользователя он свой)    user_id = msg.chat.id    # Отправляем сообщение    bot.send_message(user_id, f"Вы написали: {message}")# Запускаем бота, чтобы работал 24/7if __name__ == '__main__':    bot.polling(none_stop=True)

Мы создали эхо-бота. Подумаем, что будет в нашем боте:


  • База данных. В ней храним информацию по пользователям, а также создадим таблицу для хранения информации о пицце. Для взаимодействия с БД лучше написать отдельные классы для пользователя и пиццы.
  • Клавиатуры. Помогут сделать интерфейс удобным.
  • Оплата. Для этого будем использовать сервис QIWI.

База данных


Я пользуюсь SQLiteStudio для создания баз данных.


Структура таблицы для пользователя



  • id уникальный идентификатор пользователя. Таким выступает id пользователя в мессенджере, поэтому без зазрения совести будем использовать его.
  • stat статус работы с ботом. Запоминаем на каком шаге остановился пользователь.
  • ord. Здесь мы в виде строки храним заказ пользователя. Пока я не представляю как сделать по-другому, поэтому предлагайте идеи в комментариях.
  • random_code. В будущем будем генерировать рандомный код для оплаты через QIWI.
  • time. Пользователь выбирает, когда хочет получить заказ, а мы сохраняем в это поле.

Структура таблицы для пиццы



Данные по пиццам:



По name получаем описание пиццы и её стоимость.Осталось добавить фотографию. Создадим директорию data, поместим в нее img, а уже там будут храниться изображения.



Классы для взаимодействия с базой данных


Так, базу данных создали, теперь приступим к классам. Создадим файл user.py, в нём класс User. Сначала напишем пару функций для подключения к базе данных и закрытию соединения.


# Подключаем библиотекуimport sqlite3class User:    # Функция для соединения с БД    def connect_to_db(self):        self.connect = sqlite3.connect('PizzaApplication.db')        self.cursor = self.connect.cursor()    # Закрываем соединение с БД    def close(self):        self.connect.close()

Бота открыл пользователь, которого мы не знаем. Поэтому нужны функции добавления нового юзера и получения всех пользователей. Второе нужно для того, чтобы проверять наличие каждого написавшего "/start" в БД.


# Получаем id всех пользователей, позже будем проверять наличие пользователя в этом списке    def get_all_id(self):        self.connect_to_db()        request = "SELECT id FROM user"        result = self.cursor.execute(request).fetchall()        self.close()        return [i[0] for i in result]    # Добавляем нового пользователя    def add_id_to_db(self, user_id):        self.connect_to_db()        request = "INSERT INTO user(id, stat) VALUES(?, ?)"        self.cursor.execute(request, (user_id, "start"))        self.connect.commit()        self.close()

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


    # Получаем заданное поле по пользователю    def get_field(self, user_id, field):        self.connect_to_db()        request = f"SELECT {field} FROM user WHERE id=?"        result = self.cursor.execute(request, (user_id,)).fetchone()        self.close()        return result[0]    # Меняем значение поля    def set_field(self, user_id, field, value):        self.connect_to_db()        request = f"UPDATE user SET {field}=? WHERE id=?"        self.cursor.execute(request, (value, user_id))        self.connect.commit()        self.close()

Основные функции для пользователя написаны. Позже будем возвращаться к этому классу, а пока приступим к пицце. Создадим файл pizza.py, в нём класс Pizza. Начальные функции такие же, как у User.


import sqlite3class Pizza:    # Подключение к базе данных    def connect_to_db(self):        self.connect = sqlite3.connect('PizzaApplication.db')        self.cursor = self.connect.cursor()    # Закрытие базы данных    def close(self):        self.connect.close()

Нужно получать данные из БД.


    def get_field(self, pizza_name, field):        self.connect_to_db()        request = f"SELECT {field} FROM pizza WHERE name=?"        result = self.cursor.execute(request, (pizza_name,)).fetchone()        self.close()        return result[0]

Для админ-панели потребуется функция set_field (подобная есть в классе User), но её я пока не предусмотрел.


В следующей статье разберём клавиатуры и начнём писать код в bot.py. Спасибо за внимание!

Подробнее..

Из песочницы Разработка zond-а для замера скорости интернета

12.07.2020 00:14:47 | Автор: admin

Добрый день всем хабра-пользователям.

Постоянно читаю на хабре статьи о разработках того или иного функционала на малинке. Решил вот поделиться своей наработкой.

Предыстория


Тружусь я в компании, предоставляющей услуги кабельного телевидения и доступа в интернет. И, как это бывает в подобных компаниях, периодически слышу жалобы о несоответствии тарифного плана заявленному в договоре. То пользователь жалуется на низкую скорость по кабелю, то на высокие пинги определенных сервисов, иногда на полное отсутствие интернета в определенное время суток. Зачастую, такие жалобы попадают в пулл заявок, по которым происходит выезд на место одного из сотрудников с рабочим ноутбуком, на котором и производятся все замеры. И, зачастую, выясняется, что со скоростью все в порядке. А низкая скорость на самом деле на мобильном телефоне, через wi-fi, на балконе. Ну или нечто подобное.

К сожалению, выезжать к абоненту например в 21:37, когда у него наиболее низкие скорости не получается. Все-таки рабочий день сотрудников ограничен. Замена роутера эффекта не дает, т.к. диапазон частот для wi-fi в нашей стране плачевно захламлен.

Для справки государственный провайдер в РБ принудительно включает на всех предоставляемых в пользование устройствах wi-fi и вещает SSID ByFly с каждого устройства. Даже если у абонента нет услуги интернета, а только домашний телефон. Сделано это для дополнительных продаж. Можно в ларьке купить карту данного оператора, подключиться к любой точке с именем ByFly и, введя данные с карты, получать услуги интернета. С учетом почти 100% покрытия городов и значительным покрытием частного сектора и сельских населенных пунктов найти точку подключения не составляет проблем.

Наблюдение за нашим внешними каналами связи показывают, что имеется заданный запас полосы пропускания. И абоненты суммарно не потребляют имеющиеся каналы даже в час-пик. С этим у нас все очень серьезно. Использование разных сервисов и разных серверов замера скорости привел к интересным результатам. Оказывается не все сервисы одинаково полезны Особенно по-вечерам. И не стоит однозначно им доверять. Множество операторов той же сети Ookla не имеют широких каналов связи, либо работают впритык. А это значит, что в вечернее время получить честный результат часто практически невозможно. Да и магистралы оказывается грешат. Для примера попытки замера скорости в японию показывают крайне плачевные результаты

Первичное решение



Фото носит иллюстрационный характер

Были развернуты два сервера контроля скорости. Первый это LibreSpeed, второй Speedtest от OOKLA. Сравнивались показатели обоих сервисов. Остановиться решили все-таки на Ookla т.к. до 90% абонентов пользуются именно данным сервисом.

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

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

Ну и аналогичные действия для внешних каналов связи. Нашли больших операторов с большими каналами на speedtest серверах и написали их в рекомендациях (уж простите Moskva Rostelecom и Riga Baltcom, но буду именно данные узлы рекомендовать для получения адекватных цифр. Лично я получал до ~870 мегабит с данных серверов в часы-пик).

Зачем, спросите вы, такие сложности? Все очень просто. Мы получили достаточно удобный инструмент, который в умелых руках позволяет определить: нет ли проблем в наших сетях, нет ли проблем в республиканской сети, нет ли проблем у магистрала. Если человек жалуется на низкую скорость скачивания с какого-то сервиса мы можем сделать замер скорости канала абонента и затем сравнить с тем, что он получает от сервиса. И аргументировано показать, что мы честно выделяем канал, прописанный в договоре. А так же можем пояснить возможные причины такой разницы в скоростях.

Вторичное решение


Остается открытым вопрос падения скорости по-вечерам/в течении суток. Как сделать все то же самое не находясь у абонента дома? Взять дешевый одноплатник с гигабитной сетью и сделать из него так называемый зонд. Устройство должно с заданным интервалом времени делать замеры скорости по кабелю. Решение должно быть опенсорсное, максимально неприхотливое, с удобной админкой для просмотра результатов замеров. Устройство должно быть максимально дешевым, что бы можно было легко заменить и без опасений оставлять у абонента на n суток.

Реализация




За основу был взят BananaPI (модель M1). Причин выбора на самом деле две.

  1. Гигабитный порт.
  2. Он просто валялся в тумбочке.

Далее было принято решение использовать python клиента speedtest-cli для сервиса Speedtest by Ookla в качестве бэкэнда для замера скорости. Библиотеку Pythonping для замера скорости пинга. Ну и php для админки. Для приятности восприятия применил bootstrap.

Ввиду того, что ресурсы малинки не резиновые была использована связка nginx+php-fpm+sqlite3. От MySQL хотелось отказаться из-за ее тяжести и переизбыточности. Предугадываю вопрос относительно Iperf. От него пришлось отказаться ввиду невозможности его использования на направлениях отличных от локальных.

Изначально пошел по-пути многих на этом сайте. Модифицировал клиент speedtest-cli. Но затем, немного поразмыслив, отказался от данной затеи. Написал свой воркер, который использует возможности оригинального клиента.

Для анализа пингов просто написал отдельный обработчик. Берем среднее значение по замеру. Пинговалка умеет как ip адрес так и доменное имя.

Асинхронности работы не добивался. Она в данном случае не особо нужна.

Админка для оценки результатов получилась довольно минималистическая.

Рис. Основное окно админки с результатами тестирования

Рис. Настройки тестирования


Рис. Обновление списка серверов Speedtest

Вот собственно и все. Идея реализована на коленке, в свободное от работы время. К полевым испытаниям пока не приступили. Но планируем в ближайшее время запустить в работу опытные образцы. Использовать можно как провайдерам там и клиентам провайдеров. Никто не мешает поставить делать замеры дома круглосуточно. Единственное, следует помнить, что если вы активно серферите в сети или что-то качаете то и замер получится ниже реального. Так что в идеале нужно зонд оставлять в сети единственным потребителем трафика.

P.S.: за качество кода прошу не пинать. Я самоучка без опыта. Исходный код на GitHub. Критика принимается.
Подробнее..

Внедряем оплату BTC куда угодно (Python)

05.11.2020 18:21:17 | Автор: admin

Предыстория

Полгода назад взялся за один проект с возможностью оплаты биткойном. Так как проект делали на языке python, то и оплату хотелось реализовать на нем же. Сразу же взялся анализировать готовые решения, доступные библиотеки и Rest API Blockchain.com. С апи блокчейна я моментально обломался, так как их токен для использования апи довольно не просто получить.

Затем решил юзать различные библиотеки (block-io, bitcoinlib, blockchain и др.) После пару ночей попыток реализовать нормальную оплату, остановился на bitcoinlib, так как она более менее стабильно работала, и я спокойно переводил с одного кошелька на другой. Беда наступила когда появились первые 100 пользователей и вся оплата внезапно рухнула. Возможно я криво написал или что-то не так понял с работой библиотеки, но любые попытки восстановить работу оплаты были безуспешны, только если обнулять бдшку, но и так неизвестно сколько бы она продержалась.

В итоге решили оставить без BTC оплаты. Я опечалился и не связывался с оплатой биткойном полгода.

К чему я пришел

На днях я все-таки решил добить этот вопрос для себя, надеюсь кому-то еще пригодятся мои наработки.

Все начинается с seed фразы. Мнемоническаяфраза(англ. Mnemonic phrase илиSeed фраза) - это список слов, которые хранят всю информацию, необходимую для восстановления биткоин-кошелька. Существуют несколько стандартов генерации фраз BIP 32, BIP 39, BIP 44, и еще BIP 49. Самый распространенный - это BIP 44 (12 слов).

Пример seed фразы:

vivid area able second bicycle advance demand alpha flip stable drift route

Чтобы сгенерировать фразу будем использовать библиотеку bipwallet. Чтобы ее установить воспользуемся командой pip install bipwallet.

from bipwallet import wallet# generate 12 word mnemonic seedseed = wallet.generate_mnemonic()print(seed)

Если мы хотим получить напрямую доступ к биткойн кошельку, то, зная фразу, можем сразу восставновить кошелек в blockhain.com:

https://login.blockchain.com/#/recover

Следующим шагом мы будем преобразовывать нашу seed фразу до получения нужного адреса кошелька биткойна.

Чтобы во всем не запутаться и знать какие данные мы должны получить, я использовал сайт https://iancoleman.io/bip39/

Генерация дочернего адреса кошелька для каждого пользователя:

Чтобы получить наш нулевой адрес Биткойн кошелька на основе seed фразы (12VeK1eRgPHRUikNLXq3Nuz99gS2S46QMD), нам нужно пройти всю цепочку преобразований. Методом проб и ошибок мне все-таки удалось получить адрес кошелька следующим кодом:

from bipwallet.utils import *def gen_address(index):    # Наша seed фраза    seed = 'vivid area able second bicycle advance demand alpha flip stable drift route'    # Мастер ключ из seed фразы    master_key = HDPrivateKey.master_key_from_mnemonic(seed)    # Public key из мастер ключа по пути 'm/44/0/0/0'    root_keys = HDKey.from_path(master_key, "m/44'/0'/0'/0")[-1].public_key.to_b58check()    # Extended public key    xpublic_key = str(root_keys, encoding="utf-8")    # Адрес дочернего кошелька в зависимости от значения index    address = Wallet.deserialize(xpublic_key, network='BTC').get_child(index, is_prime=False).to_address()    rootkeys_wif = HDKey.from_path(master_key, f"m/44'/0'/0'/0/{index}")[-1]    # Extended private key    xprivatekey = str(rootkeys_wif.to_b58check(), encoding="utf-8")    # Wallet import format    wif = Wallet.deserialize(xprivatekey, network='BTC').export_to_wif()    return address, str(wif, 'utf-8')print(gen_address(0))

Данная функция возвращает адрес кошелька и wif в зависимости номера. Максимальное число с которым удалось получить адрес это 999999999.

wif (Wallet import format) - это просто кодирование байтов ключа в кодировку Base58 + контрольная сумма. Он нам понадобится в дальнейшем при генерации транзакции.

Это все значит, что имея только одну seed фразу мы можем создать 1 млрд дочерних адресов. Каждому пользователю при регистрации мы будем выдавать новый адрес, через который он сможет оплачивать по BTC. Появляется ограничение на 1 млрд пользователей, но нам никто не запрещает использовать несколько seed фраз или генерировать каждому юзеру новую фразу, но тогда каждая оплата будет кидаться не в общий ваш кошелек, а по разным.

Проверка баланса и транзакции:

Теперь когда у каждого пользователя свой личный адрес биткойн кошелька, нужно проверить баланс этого адреса. Для этого мы будем обращаться к сайту Blockchain.com дабы получить нужную информацию.

import requests# Адрес кошелька пользователя wallet = '12VeK1eRgPHRUikNLXq3Nuz99gS2S46QMD'# wallet = gen_address(0)url = f'https://blockchain.info/rawaddr/{wallet}'x = requests.get(url)wallet = x.json()print('Итоговый баланс:'+str(wallet['final_balance']))print('Транзакции:'+str(wallet['txs']))if wallet['total_received']==0:  print('баланс пустой')

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

Транзакции

На данном этапе мы дали каждому пользователю свой адрес кошелька и знаем все транзакции с данным адресом, но этого недостаточно. Нам нужно чтобы мы могли отправить его же деньги обратно. Для этого воспользуемся библотекой bit. Чтобы ее установить воспользуемся командой pip install bit.

from bit import PrivateKey# Приватный ключ из wifmy_key = PrivateKey(wif='L46ixenNSu8Bqk899ZrH8Y96t8DHqJ1ZyxzQBGFTbh38rLHLaPoY')# Количество долларов перевода, можно поменять на btcmoney=0.1# Кошелек куда будут переведены деньгиwallet='17ya3bCpPioyPH8kAyFkEDBUqdjF6wwPxo'# Коммисия перевода, если поставить слишком маленькую, то транзакцию не примут# И чем больше коммисия, тем быстрее пройдет переводfee=2000# Генерация транзакцииtx_hash = my_key.create_transaction([(wallet, money, 'usd')],fee=fee,absolute_fee=True)print(tx_hash)

В итоге мы получили вот такую транзакцию:

0100000001fe64490fce5e85d5eb00865663a3d44f4108549fdb2840b086cfc781390d4a2d010000006a47304402202dc1496d28bb10d50d94d70870e2a79ea472c5960de8f7418bb30f9b96643efc02204691547c98edad3181a056bf6404601efe289200ba8e3073a2f5b7c0c7f4fec10121026516c551584b484ce3ca7bb71bbf24cce133bf40bdf4e2ce5a3936bc7e66a2abffffffff02e3020000000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac08f40200000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac00000000

Выглядит красиво, но что с этим делать?

Можно зайти на сайт https://www.blockchain.com/btc/pushtx

и вручную отправить эту транзакцию.

Также можем декодировать эту транзакцию и проверить все ли верно мы указали https://www.blockchain.com/btc/decode-tx

Но нам нужно это автоматизировать, поэтому напишем несколько строк:

import requestsurl = 'https://blockchain.info/pushtx'tx='0100000001fe64490fce5e85d5eb00865663a3d44f4108549fdb2840b086cfc781390d4a2d010000006a47304402202dc1496d28bb10d50d94d70870e2a79ea472c5960de8f7418bb30f9b96643efc02204691547c98edad3181a056bf6404601efe289200ba8e3073a2f5b7c0c7f4fec10121026516c551584b484ce3ca7bb71bbf24cce133bf40bdf4e2ce5a3936bc7e66a2abffffffff02e3020000000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac08f40200000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac00000000'x = requests.post(url, data = {'tx':tx})result = x.textprint(result)

Выполним пост запрос, если получаем ответ: Transaction Submitted. Это значит, что через несколько секунд транзакция появится в сети и деньги спишутся с пользователя.

Применение

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

Для демонстрации работы BTC оплаты, я напишу простенького телеграм бота, который будет выполнять роль клиента Blockchain.com, то есть вы сможете хранить в нем свои биткойны и от туда же переводить другим людям. Ссылка на исходники бота будут в конце.

Проверить работу бота можно тут: https://t.me/Blockchain_client_bot

Задеплоил на heroku, так что надеюсь не будет падать)

Функционал бота

Регистрация пользователя

В качестве БД я использовал sqlite3 и создал одну таблицу пользователей:

import sqlite3conn = sqlite3.connect("my.db")  # или :memory: чтобы сохранить в RAMcursor = conn.cursor()cursor.execute("CREATE TABLE users (chatid INTEGER , name TEXT, balance INTEGER, btc_wallet TEXT, wif TEXT, btc_sent TEXT, state INTEGER)")conn.commit()

При нажатии start мы регистрируем пользователя, генерируем для него адрес биткойн кошелька, wif и добавляем данные в БД:

sql = "SELECT COUNT(*) FROM users "cursor.execute(sql)user = cursor.fetchone()  address, wif= gen_address(user[0]+1)sql_insert = "INSERT INTO users VALUES ({}, '{}', 0,'{}','{}','no',0)".format(message.chat.id,                                                                           message.chat.first_name,address,wif)cursor.execute(sql_insert)conn.commit()

Проверка баланса

if message.text == '? Ваш баланс':  url = f'https://blockchain.info/rawaddr/{data[3]}'  x = requests.get(url)  wallet = x.json()  await bot.send_message(message.chat.id, f'''? *Итоговый баланс:* {format(wallet['final_balance'] / 100000000, '.9f')} BTC*Всего получено:* {format(wallet['total_received'] / 100000000, '.9f')} BTC*Всего отправлено:* {format(wallet['total_sent'] / 100000000, '.9f')} BTChttps://www.blockchain.com/ru/btc/address/{data[3]}''', parse_mode= "Markdown")

Получить BTC

Для создания qr-кода я использовал библиотеку qrcode и на вход передал ранее сгенерированный адрес биткойн кошелька из БД.

 if message.text == '? Получить BTC':    img = qrcode.make(data[3])    img.save('qr.jpg')    await bot.send_message(message.chat.id, f'''? Ваш адрес биткойн кошелька:*{data[3]}*''', parse_mode= "Markdown")        await bot.send_photo(message.chat.id,photo=open('qr.jpg', 'rb'))

Отправить BTC

try:    sum = float(message.text)    url = f'https://blockchain.info/rawaddr/{data[3]}'    x = requests.get(url)    wallet = x.json()    if sum + 10000 &lt;= wallet['final_balance'] / 100000000:        try:            my_key = PrivateKey(wif=data[4])            # Коммисия перевода, если поставить слишком маленькую, то транзакцию не примут            # И чем больше коммисия, тем быстрее пройдет перевод            fee = 10000            # Генерация транзакции            tx_hash = my_key.create_transaction([(data[5], sum, 'btc')], fee=fee, absolute_fee=True)            print(tx_hash)            url = 'https://blockchain.info/pushtx'            x = requests.post(url, data={'tx': tx_hash})            result = x.text            sql = "UPDATE users SET state = {} WHERE chatid = {}".format(0, message.chat.id)            cursor.execute(sql)            conn.commit()            await bot.send_message(message.chat.id, result)        except Exception:            await bot.send_message(message.chat.id, " Ошибка при выолнении транзакции")    else:        await bot.send_message(message.chat.id, '  На вашем балансе недостаточно средств.')except ValueError:    await bot.send_message(message.chat.id, 'Неправильно введена сумма отправления, попробуйте еще раз')

Проверим через сайт, что транзакция отправилась:

Исходники и как запустить

Скачать исходники бота можно тут: github.com/Lil-hack/blockchain-client

Склонировав репозиторий, устанавливаем необходимые пакеты:

pip install-rrequirements.txt

Некоторые библиотеки у меня не заработали на windows, так что лучше сразу запускать на linux.

В файле main.py заменяем ваш токен телеграм бота:

# Ваш токен от BotFatherTOKEN = 'YOUR TOKEN'

В файле btc_core.py заменяем на вашу seed фразу:

# Ваша seed фразаseed = 'YOUR SEED'

И запускаем бота командой: python main.py

Работает на python 3.7.0 и выше. Бот написан за один вечер, так что просьба строго не судить ^^

Итого

Как оказалось, все довольно не сложно, и в несколько десятков строк можно добавить оплату BTC в любой python проект. Я не профи в криптографии, так что скорее всего многие моменты упустил, но надеюсь кому-то эта статья будет полезна.

Подробнее..

PySpark. Решаем задачу на поиск сессий

07.03.2021 10:21:03 | Автор: admin

Добрый день уважаемые читатели! Несколько дней назад перечитывая книгу Энтони Молинаро SQL. Сборник рецептов, в одной из глав я наткнулся на тему, которая была посвящена определению начала и конца диапазона последовательных значений. Бегло ознакомившись с материалом, я сразу вспомнил, что уже сталкивался с данным вопросом в качестве одного из тестовых заданий, но тогда тема была заявлена как Задача на поиск сессий. Фишкой технического собеседования был не разбор выполненной работы, а один из вопросов интервьюера о том, как получить аналогичные значения с помощью Spark. Готовясь к собеседованию, я не знал, что в компании применяется (а может и не применяется) Apache Spark, и поэтому не собрал информацию по новому на тот момент для меня инструменту. Оставалось лишь выдвинуть гипотезу, что искомое решение может быть подобно скрипту, который можно написать c помощью библиотеки Pandas. Хотя очень отдалено я все-таки попал в цель, однако поработать в данной организации не получилось.

Справедливости ради хочу заметить, что за прошедшие годы я несильно продвинулся в изучении Apache Spark. Но я все равно хочу поделиться с читателями наработками, так как многие аналитики вообще не сталкивались с этим инструментом, а другим возможно предстоит подобное собеседование. Если вы являетесь профессионалом Spark, то всегда можно предложить более оптимальный код в комментариях к публикации.

Это была преамбула, приступим непосредственно к разбору данной темы. Пойдем сначала и напишем SQL скрипт. Но прежде создадим базу данных и заполним ее значениями. Так как это демо-пример предлагаю использовать SQLite. Данная БД уступает более мощным коллегам по цеху, но ее возможностей для разработки скрипта нам хватит сполна. Чтобы автоматизировать заявленные выше операции, я написал вот такой код на Python.

# Импорт библиотекimport sqlite3# Данные для записи в БДprojects = [    ('2020-01-01', '2020-01-02'),    ('2020-01-02', '2020-01-03'),    ('2020-01-03', '2020-01-04'),    ('2020-01-04', '2020-01-05'),    ('2020-01-06', '2020-01-07'),    ('2020-01-16', '2020-01-17'),    ('2020-01-17', '2020-01-18'),    ('2020-01-18', '2020-01-19'),    ('2020-01-19', '2020-01-20'),    ('2020-01-21', '2020-01-22'),    ('2020-01-26', '2020-01-27'),    ('2020-01-27', '2020-01-28'),    ('2020-01-28', '2020-01-29'),    ('2020-01-29', '2020-01-30')]try:    # Создаем соединение    con = sqlite3.connect("projects.sqlite")    # Создаем курсор    cur = con.cursor()    # Создаем таблицу    cur.execute("""CREATE TABLE IF NOT EXISTS projects (                    proj_id INTEGER PRIMARY KEY AUTOINCREMENT,                    proj_start TEXT,                    proj_end TEXT)""")    # Добавляем записи    cur.executemany("INSERT INTO projects VALUES(NULL, ?,?)", projects)    # Сохраняем транзакцию    con.commit()    # Закрываем курсор    cur.close()except sqlite3.Error as err:    print("Ошибка выполнения запроса", err)finally:    # Закрываем соединение    con.close()    print("Соединение успешно закрыто")

Решение тривиальное и не требует дополнительных комментариев. Для коммуникации с созданной БД я использовал DBeaver. Клиентское приложение хорошо себя зарекомендовало, поэтому я часто использую его для разработки SQL запросов.

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

select       p3.proj_group,       min(p3.proj_start) as date_start,      max(p3.proj_end) as date_end,      julianday(max(p3.proj_end))-julianday( min(p3.proj_end))+1 as deltafrom    (select      p2.*,     sum(p2.flag)over(order by p2.proj_id) as proj_groupfrom (select       p.proj_id ,       p.proj_start,       p.proj_end,       case       when lag(p.proj_end)over(order by p.proj_id) = p.proj_start then 0 else 1       end as flagfrom projects as p) as p2) as p3group by p3.proj_group

Если вы раньше уже использовали оконные функции, то разобраться самостоятельно с написанной конструкцией не составит никакого труда. Я лишь кратко опишу логику. Первоначальная таблица представляет собой последовательные шаги, для которых заданы два параметра: дата начала и дата конца. Если дата начала шага соответствует дате конца предыдущего шага, то два шага считаются одной сессией. Следовательно, начинать расчеты нужно со смещения, за это отвечает оконная функция lag. На следующем этапе сравниваем дату старта текущего шага и дату конца предыдущего и выводим либо 0, либо 1. Если к новому столбцу применить суммирование с нарастающим итогом, то получим номера сессий. Стандартная группировка по номерам с агрегирующими функциями позволит извлечь начало и конец диапазона значений. Я также рассчитал дельту между двумя датами на случай, если потребуется установить самую длинную или короткую сессию. Приведенный код будет актуален и для других БД. Ошибка будет выводиться только на строчке, где находится разница между двумя датами (функция julianday это прерогатива SQLite). На этом первая часть тестового задания выполнена. Переходим к Spark.

Если верить Википедии, то Apache Spark это фреймворксоткрытым исходным кодомдля реализации распределённой обработкинеструктурированныхи слабоструктурированных данных, входящий в экосистему проектовHadoop. Так как я не пишу на Java, Scala или R, то для получения функциональности Spark решил использовать PySpark. Устанавливать на компьютер все необходимые для работы компоненты я не стал. Для экспериментов выбрал облачный сервис Google Colab, так как у меня уже был заведенный аккаунт. Основной минус - при каждом новом сеансе работы нужно заново скачивать файлы, связанные с запуском нашего инструмента. На просторах Интернета я встречал вариант с фиксированной установкой, но пока не пробовал его на практике.

С помощью базовых команд Linux мы устанавливаем OpenJDK, скачиваем и разархивируем файлы Spark. Затем прописываем две переменные среды. Нужно не забыть о вспомогательной библиотеке findspark. Подготовительная работа закончена, осталось только открыть сессию.

В идеале следует импортировать файл с БД SQLite в облако и подключаться к нему, но я решил облегчить себе жизнь и сформировал датафрейм прямо в ноутбуке. Чтобы даты воспринимались как даты, потребовалось написать собственную функцию.

Так как операций в Spark довольно много, рекомендую сразу обзавестись шпаргалками. Если говорить о литературе для изучения данного инструмента, то радует два факта. Во-первых, есть как англоязычные, так и переводные издания, а во-вторых, источников информации предостаточно. Если вы не владеете языком Шекспира, то могу порекомендовать в первую очередь Изучаем Spark. Молниеносный анализ данных, авторы Холден Карау, Энди Конвински, Патрик Венделл, Матей Захария.

После того, как датафрейм подготовлен, можно пойти сначала самым простым путем и использовать уже имеющийся скрипт SQL. В код нужно лишь внести две правки: изменить имя базовой таблицы, изменить способ нахождения дельты (функция datediff).

Строго говоря, мы уже выполнили вторую часть тестового задания. Но, что-то мне подсказывает, что на собеседовании к данному способу могут придраться, аргументируя это тем, что это все тот же скрипт SQL без специфики Spark. Поэтому в конце статьи приведу псевдокод, который скорее всего уступает по производительности предыдущему решению, но лучше отражает способности соискателя программировать. Полную версию скрипта можно найти в ноутбуке.

from pyspark.sql.functions import lagfrom pyspark.sql import functions as Ffrom pyspark.sql.window import Window# Equivalent of Pandas.dataframe.shift() methodw = Window().partitionBy().orderBy(col("proj_id"))df_dataframe = df.withColumn('lag', F.lag("proj_end").over(w))#...# Equivalent of SQL- CASE WHEN...THEN...ELSE... ENDdf_dataframe = df_dataframe.withColumn('flag',F.when(df_dataframe["proj_start"] == df_dataframe["lag"],0).otherwise(1))#...# Cumsum by column flagw = Window().partitionBy().orderBy(col("proj_id"))df_dataframe = df_dataframe.withColumn("proj_group", F.sum("flag").over(w))#...# Equivalent of SQL - GROUP BYfrom pyspark.sql.functions import  min, maxdf_group = df_dataframe.groupBy("proj_group").agg(min("proj_start").alias("date_start"), \                                                  max("proj_end").alias("date_end"))df_group = df_group.withColumn("delta", F.datediff(df_group.date_end,df_group.date_start))df_group.show()

Краткие выводы.

  1. Перед собеседованием внимательно изучайте список инструментов и технологий, с которыми работает компания. Не стоит ограничиваться позициями только из текста вакансии. В идеале нужно постоянно расширять свой профессиональный кругозор, чтобы отвечать на каверзные вопросы, которые задаются с цель всесторонне прощупать кандидата.

  2. Даже если вы раньше никогда не работали со Spark, это не повод отказываться от конкурса на вакантную позицию. Основы PySpark можно освоить в сжатые сроки, при условии, что в бэкграунде уже есть опыт программирования с использованием библиотеки Pandas.

  3. Недостатка в книгах по Spark не наблюдается.

На этом все. Всем здоровья, удачи и профессиональных успехов!

Подробнее..
Категории: Python , Sql , Big data , Data engineering , Sqlite , Spark

Подключаем Sqlite3 к Telegram боту

16.04.2021 16:20:16 | Автор: admin

Вступление

Для многих новичков в разработке на Python возникает проблема как подключить базу данных? Я сам столкнулся с такой проблемой в начале разработки. Тема оказалось довольно простой, но в интернете есть множество гайдов, которые могут запутать. В этом туториале я расскажу о том, как просто интегрировать базу данных Sqlite3 на примере Telegram бота.

Начало работы

Для работы нам понадобятся базовые знания в Python, достаточные для написания простого бота. Данный гайд можно использовать не только для интеграции с ботом, но и для других проектов. Если вы не используете PyCharm, и у вас нет средства для работы с БД, то необходимо установить утилиту SQLiteStudio с официального сайта.

Теперь давайте перейдем к написанию кода. В терминале необходимо установить pyTelegramBotAPI (если по какой-то причине у вас его нет).

pip install pyTelegramBotAPI

Далее создаем новый файл с расширением .py. В нем прописываем следующее:

import sqlite3import telebotbot = telebot.TeleBot("token")

Устанавливать sqlite3 не нужно это стандартная библиотека. Вместо "token" необходимо указать токен бота, который выдал вам BotFather. Теперь проверим, работает ли бот.

@bot.message_handler(commands=['start'])def start_message(message):bot.send_message(message.chat.id, 'Добро пожаловать')

Запустим код. Если при запуске бота он пишет нам "Добро пожаловать", то все работает отлично.

Как мы видим, все работает.

Настройка базы данных

Открываем SQLiteStudio. Во вкладке Database выбираем пункт Add a database. Для удобства можно использовать сочетание Ctrl+O.

Далее нажимаем на зеленую кнопку Создать новый фал базы данных. Выбираем директорию вашего проекта. В ней можете создать отдельную папку db, где будет хранится файл с базой. Вводим название, например database. Расширение будет присвоено автоматически. Нажимаем Сохранить. Теперь слева в списке у нас появился наш файл. Кликаем по нему ПКМ и выбираем пункт Connect to the database. Соединение с базой установлено. Появилось два подпункта: Таблицы и Представления. В этом уроке мы будем затрагивать только пункт Таблицы. Нажимаем по нему ПКМ и выбираем Add a table.

В поле для ввода указываем имя для таблицы. Я назову ее test. Чуть выше есть активная кнопка Добавить столбец (Ins). В появившемся окне указываем название столбца и тип данных. Для начала вам могут понадобится такие условия, как Первичный ключ, Не NULLи Уникальность. Первый столбец я назову id, выберу тип данных INTEGER и установлю все три условия. У Первичный ключ выберу настройку Автоинкремент. Этот столбец будет автоматически создавать ID записи в таблице. Работать с ним в коде мы не будем.

Второй столбец будет хранить user_id. Он будет иметь тип данных INT, уникален и не равен нулю. Третий столбец я назову user_name, присвою тип данных STRING и выберу пункт Не NULL. Еще 2 столбца будут называться user_surname и username. После того, как вы создали столбцы, нажимаем зеленую кнопку с галочкой.

В общем все будет выглядеть вот так.

Работа с базой данных в коде

Теперь давайте вновь перейдем к коду. Создадим 2 переменные.

conn = sqlite3.connect('db/database.db', check_same_thread=False)cursor = conn.cursor()

В первой мы подключаем нашу базу данных, а во второй создаем курсор для работы с таблицами. Обратите внимание на то, что у меня в директории проекта создана папка db, а уже в ней находится файл БД. Если у вас по другому, укажите расположение в первой переменной.

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

def db_table_val(user_id: int, user_name: str, user_surname: str, username: str):cursor.execute('INSERT INTO test (user_id, user_name, user_surname, username) VALUES (?, ?, ?, ?)', (user_id, user_name, user_surname, username))conn.commit()

Итак, сейчас объясню, что тут происходит. В аргументах функции мы указываем переменную user_id с типом данных inst. Далее добавляем запись в таблицу test в столбец user_id. С остальными значениями точно так же. В конце мы просто применяем изменения. Эта функция не будет выполнятся, пока мы ее не вызовем. Давайте исправим это. Создадим обработчик сообщений от пользователя и будем вносить в базу его данные.

@bot.message_handler(content_types=['text'])def get_text_messages(message):if message.text.lower() == 'привет':bot.send_message(message.from_user.id, 'Привет! Ваше имя добавленно в базу данных!')        us_id = message.from_user.idus_name = message.from_user.first_nameus_sname = message.from_user.last_nameusername = message.from_user.usernamedb_table_val(user_id=us_id, user_name=us_name, user_surname=us_sname, username=username)

В переменные мы вносим имя пользователя, id, имя и фамилию в Telegram, а потом сохраняем в БД. В конце кода можем сделать так, чтобы бот работал постоянно.

bot.polling(none_stop=True)

Пришло время проверить, как работает код. Запускаем бота и пишем ему "Привет".

Теперь можем зайти в SQLiteStudio, и выбрав вкладку Данные посмотреть что получилось:

Как мы видим, данные уже в базе. Если их по какой-то причине нет, то обновите базу, нажав на синюю кнопку Обновить

Вот весь код:

import sqlite3import telebotbot = telebot.TeleBot("token")conn = sqlite3.connect('db/database.db', check_same_thread=False)cursor = conn.cursor()def db_table_val(user_id: int, user_name: str, user_surname: str, username: str):cursor.execute('INSERT INTO test (user_id, user_name, user_surname, username) VALUES (?, ?, ?, ?)', (user_id, user_name, user_surname, username))conn.commit()@bot.message_handler(commands=['start'])def start_message(message):bot.send_message(message.chat.id, 'Добро пожаловать')@bot.message_handler(content_types=['text'])def get_text_messages(message):if message.text.lower() == 'привет':bot.send_message(message.chat.id, 'Привет! Ваше имя добавлено в базу данных!')us_id = message.from_user.idus_name = message.from_user.first_nameus_sname = message.from_user.last_nameusername = message.from_user.usernamedb_table_val(user_id=us_id, user_name=us_name, user_surname=us_sname, username=username)bot.polling(none_stop=True)

Заключение

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

Огромное спасибо за то, что прочитали. Надеюсь, кому-то помог. Всем удачи и поменьше багов в коде!

Подробнее..
Категории: Python , Api , Sqlite , Telegram

Очередная причуда Win 10 и как с ней бороться

13.05.2021 00:05:04 | Автор: admin

Квалификацию надо иногда повышать, и вообще учиться для мозгов полезно. А потому пошел я недавно на курсы - поизучать Python и всякие его фреймворки. На днях вот до Django добрался. И тут мы в ходе обучения коллективно выловили не то чтобы баг, но дивный эффект на стыке Python 3, Sqlite 3, JSON и Win 10. Причем эффект был настолько дивен, что гугль нам не помог - пришлось собираться всей заинтересованной группой вместе с преподавателем и коллективным разумом его решать.
А дело вот в чем: изучали мы базу данных (а у Django предустановлена Sqlite 3) и, чтоб каждый раз заново руками данные не вбивать, прикрутили загрузку скриптом из json-файлов. А в файлы данные из базы штатно дампили питоновскими же методами:

python manage.py dumpdata -e contenttypes -o db.json

Внезапно те, кто работал под виндой (за все версии не поручусь, у нас подобрались только обитатели Win 10), обнаружили, что дамп у них производится в кодировке windows-1251. Более того, джейсоны в этой кодировке отлично скармливаются базе. Но стоило только переформатировать их в штатную по документам для Sqlite 3, Python 3 и особенно для JSON кодировку UTF-8, как в лучшем случае кириллица в базе превращалась в тыкву, а в худшем ломался вообще весь процесс загрузки данных.
Ничего подобного найти не удалось ни в документации, ни во всем остальном гугле, считая и англоязычный. Что самое загадочное, ручная загрузка тех же самых данных через консоль или админку проекта работала как часы, хотя уж там-то кодировка была точно UTF-8. Более того, принудительное прописывание кодировки базе никакого эффекта не дало.
Мы предположили, что причиной эффекта было взаимодействие джейсона с операционной системой - каким-то образом при записи и чтении именно джейсонов система навязывала свою родную кодировку вместо нормальной. И действительно, когда при открытии файла принудительно устанавливалась кодировка UTF-8:

open(os.path.join(JSON_PATH, file_name + '.json'), 'r', encoding="utf-8")

в базу попадали не кракозябры, а нормальные русские буквы. Но проблему с созданием дампа таким способом не решишь, а переделывать кодировку потом руками тоже как-то не по-нашему. И тогда мы решили поискать способ укротить винду.
И такой способ нашелся. Вот он:

  • открываем панель управления, но не новую красивую, а старую добрую:

  • открываем (по стрелке) окошко региона:

  • по стрелкам переключаем вкладку "Дополнительно" и открываем окошко "Изменить язык системы":

  • и в нем ставим галку по стрелке в чекбоксе "Бета-версия: Использовать Юникод (UTF-8) для поддержки языка во всем мире.

Система потребует перезагрузки, после чего проблема будет решена.

Не могу сказать, чтобы этот мелкий странный баг был так уж важен или интересен (питоновские проекты обычно живут под линуксами, где такого не бывает), но мозги он нам поломал изрядно - вследствие чего я и решил написать эту заметку. Мало ли кто еще из новичков как раз во время учебы попадется.

Подробнее..
Категории: Python , Python3 , Sqlite , Json , Windows 10 , Encodings

Сколько данных может обработать Raspberry Pi быстро

02.07.2020 00:11:00 | Автор: admin
Время обработки данных в одном знакомом мне проекте энтерпрайз-хранилища данных с реляционной моделью составляет почти 6 часов. Много это или мало?

Заметка описывает эксперимент по созданию маленькой копии энтерпрайз-хранилища данных с сильно ограниченными техническими условиями. А именно, на базе одноплатного компьютера Raspberry Pi.
Модель и архитектура будут упрощёнными, но похожими на энтерпрайз-хранилище. Результатом является оценка возможности использования Raspberry Pi в области обработки и анализа данных.




1


Роль опытного и сильного игрока будет выполнять машина Exadata Х5 (один юнит) корпорации Оракл.
Процесс обработки данных включает в себя следующие шаги:

  • Чтение из файла 10,3 ГБ 350 миллионов записей за 180 минут.
  • Обработка и очистка данных 2 SQL запроса и 130 минут.
  • Загрузка измерений 10 минут.
  • Загрузка таблиц фактов 20 миллионами новых записей 5 SQL запросов и 35 минут.

Итого, интеграция 350 миллионов записей за 6 часов, что эквивалентно 990 тысячам записей в минуту, или примерно 17 тысячам записей исходных данных в секунду.

2


В роли экспериментального оппонента будет выступать Raspberry Pi 3 Model B+ с 4х-ядерным процессором 1.4 ГГц.
В качестве хранилища используется sqlite3, чтение файлов происходит с помощью PHP.

Модель данных в реляционной базе sqlite3 описана в статье о маленьком хранилище.

Тест первый


Исходный файл access.log 37 МБ с 200 тысячами записей.

  • Прочитать лог и записать в базу данных заняло 340 секунд.
  • Загрузка измерений с 5 тысячами записей длилась 5 секунд.
  • Загрузка таблиц фактов 90 тысячами новых записей 32 секунды.

Итого, интеграция 200 тысяч записей заняла почти 7 минут, что эквивалентно 28 тысячам записей в минуту, или 470 записям исходных данных в секунду. База данных занимает 7,5 МБ; всего 8 SQL запросов для обработки данных.

Тест второй


Файл более активного сайта. Исходный файл access.log 67 МБ с 290 тысячами записей.

  • Прочитать лог и записать в базу данных заняло 670 секунд.
  • Загрузка измерений с 25 тысячами записей длилась 8 секунд.
  • Загрузка таблиц фактов 240 тысячами новых записей 80 секунд.

Итого, интеграция 290 тысяч записей заняла чуть больше 12 минут, что эквивалентно 23 тысячам записей в минуту, или 380 записям исходных данных в секунду. База данных занимает 22,9 МБ

Вывод


Для получения данных в виде модели, которая позволит проводить эффективный анализ, необходимы значительные вычислительные и материальные ресурсы, и время в любом случае.
Например, один юнит Экзадаты обходится более чем в 100К. Один Raspberry Pi стоит 60 единиц.
Линейно их нельзя сравнивать, т.к. с увеличением объёмов данных и требований надёжности возникают сложности.

Однако, если представить себе случай, когда тысяча Raspberry Pi работают параллельно, то, исходя из эксперимента, они обработают около 400 тысяч записей исходных данных в секунду.
И если решение для Экзадаты оптимировать до 40 тысяч записей в секунду, то это ощутимо меньше, чем 400 тысяч. Это подтверждает внутреннее ощущение того, что цены энтерпрайз-решений завышены.

В любом случае, Raspberry Pi отлично справлятся с обработкой данных и реляционными моделями соответствующего масштаба.

Ссылка


Домашний Raspberry Pi был настроен как веб сервер. Эксперимент с его производительностью можно провести самостоятельно по адресу. Модель базы данных (DDL), процедуры загрузки (ETL) и саму базу данных можно там же скачать.
Подробнее..

Возможности SQLite, которые вы могли пропустить

20.11.2020 10:22:07 | Автор: admin
Если вы используете SQLite, но не следите за его развитием, то возможно некоторые вещи, позволяющие сделать код проще, а запросы быстрее, прошли незамеченными. Под катом я постарался перечислить наиболее важные из них.

Частичные индексы (Partial Indexes)

При построении индекса можно указать условие попадания строки в индекс, к примеру, одна из колонок не пустая, а другая равна заданному значению.

create index idx_partial on tab1(a, b) where a is not null and b = 5;select * from tab1 where a is not null and b = 5; --> search table tab1 using index

Индексы на выражение (Indexes On Expressions)

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

create index idx_expression on tab1(a + b);select * from tab1 where a + b > 10; --> search table tab1 using index ...select * from tab1 where b + a > 10; --> scan table

Вычисляемые колонки (Generated Columns)

Если данные столбца представляют собой результат вычисления выражения по другим столбцам, то можно создать виртуальный столбец. Есть два вида: VIRTUAL (вычисляется каждый раз при чтении таблицы и не занимает места) и STORED (вычисляется при записи данных в таблицу и место занимает). Разумеется записывать данные в такие столбцы напрямую нельзя.

create table tab1 (a integer primary key,b int,c text,d int generated always as (a * abs(b)) virtual,e text generated always as (substr(c, b, b + 1)) stored);

R-Tree индекс

Индекс предназначен для быстрого поиска в диапазоне значений/вложенности объектов, т.е. задачи типичной для гео-систем, когда объекты-прямоугольники заданы своей позицией и размером и требуется найти все объекты, которые пересекаются с текущим. Данный индекс реализован в виде виртуальной таблицы (см. ниже) и это индекс только по своей сути. Для поддержки R-Tree индекса требуется собрать SQLite с флагом SQLITE_ENABLE_RTREE (по умолчанию не установлен).

create virtual table idx_rtree using rtree (id,              -- ключminx, maxx,      -- мин и макc x координатыminy, maxy,      -- мин и макc y координатыdata             -- дополнительные данные  );  insert into idx_rtree values (1, -80.7749, -80.7747, 35.3776, 35.3778); insert into idx_rtree values (2, -81.0, -79.6, 35.0, 36.2);select id from idx_rtree where minx >= -81.08 and maxx <= -80.58 and miny >= 35.00  and maxy <= 35.44;

Переименование колонки

В SQLite слабо поддерживает изменения в структуре таблиц, так, после создания таблицы, нельзя изменить ограничение (constraint) или удалить столбец. С версии 3.25.0 можно переименовать столбец, но не изменить его тип.

alter table tbl1 rename column a to b;

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

Добавить строку, иначе обновить (Upsert)

Используя класс on conflict оператора insert, можно добавить новую строку, а при уже имеющейся с таким же значением по ключу, обновить.

create table vocabulary (word text primary key, count int default 1);insert into vocabulary (word) values ('jovial')   on conflict (word) do update set count = count + 1;

Оператор Update from

Если строка должна быть обновлена на основе данных другой таблицы, то ранее приходилось использовать вложенный запрос для каждого столбца или with. С версии 3.33.0 оператор update расширен ключевым словом from и теперь можно делать так

update inventory   set quantity = quantity - daily.amt  from (select sum(quantity) as amt, itemid from sales group by 2) as daily where inventory.itemid = daily.itemid;

CTE запросы, класс with (Common Table Expression)

Класс with может использоваться как временное представление для запроса. В версии 3.34.0 заявлена возможность использования with внутри with.

with tab2 as (select * from tab1 where a > 10),   tab3 as (select * from tab2 inner join ...)select * from tab3;

С добавлением ключевого слова recursive, with можно использовать для запросов, где требуется оперировать связанными данными.

-- Генерация значенийwith recursive cnt(x) as (  values(1) union all select x + 1 from cnt where x < 1000)select x from cnt;-- Нахождения дочерних элементов или родителя в таблице с иерархиейcreate table tab1 (id, parent_id);insert into tab1 values   (1, null), (10, 1), (11, 1), (12, 10), (13, 10),  (2, null), (20, 2), (21, 2), (22, 20), (23, 21);-- Узлы ниже по иерархииwith recursive tc (id) as (select id from tab1 where id = 10union select tab1.id from tab1, tc where tab1.parent_id = tc.id)-- Узелы верхнего уровня для выбранных дочернихwith recursive tc (id, parent_id) as (select id, parent_id from tab1 where id in (12, 21)union select tc.parent_id, tab1.parent_id from tab1, tc where tab1.id = tc.parent_id)select distinct id from tc where parent_id is null order by 1;-- Формирования отступов при выводе, напр. для структуры отделовcreate table org(name text primary key, boss text references org);insert into org values ('Alice', null),   ('Bob', 'Alice'), ('Cindy', 'Alice'), ('Dave', 'Bob'),   ('Emma', 'Bob'), ('Fred', 'Cindy'), ('Gail', 'Cindy');with recursive  under_alice (name, level) as (    values('Alice', 0)    union all    select org.name, under_alice.level + 1      from org join under_alice on org.boss = under_alice.name     order by 2  )select substr('..........', 1, level * 3) || name from under_alice;

Оконные функции (Window Functions)

С версии 3.25.0 в SQLite доступны оконные функции, также иногда называемые аналитическими, позволяющие проводить вычисления над частью данных (окном).

-- Номер строки в результатеcreate table tab1 (x integer primary key, y text);insert into tab1 values (1, 'aaa'), (2, 'ccc'), (3, 'bbb');select x, y, row_number() over (order by y) as row_number from tab1 order by x;-- Таблица используется для следующих примеровcreate table tab1 (a integer primary key, b, c);insert into tab1 values (1, 'A', 'one'),  (2, 'B', 'two'), (3, 'C', 'three'), (4, 'D', 'one'),   (5, 'E', 'two'), (6, 'F', 'three'), (7, 'G', 'one');-- Доступ к предыдущей и следующей записи в окнеselect a, b, group_concat(b, '.') over (order by a rows between 1 preceding and 1 following) as prev_curr_next from tab1;-- Значения в окне (группе, определяемой колонкой c)  от текущей строки до конца окнаselect c, a, b, group_concat(b, '.') over (partition by c order by a range between current row and unbounded following) as curr_end from tab1 order by c, a;-- Пропуск строк в окне по условиюselect c, a, b, group_concat(b, '.') filter (where c <> 'two') over (order by a) as exceptTwo from t1 order by a;

Утилиты SQLite

Помимо CLI sqlite3 доступны еще две утилиты. Первая sqldiff, позволяет сравнивать базы (или отдельную таблицу) не только по структуре, но и по данным. Вторая sqlite3_analizer используется для вывода информации о том, как эффективно используется место таблицами и индексами в файле базы данных. Аналогичную информацию можно получить из виртуальной таблицы dbstat (требует флаг SQLITE_ENABLE_DBSTAT_VTAB при компиляции SQLite).

С версии 3.22.0 CLI sqlite3 содержит (экспериментальную) команду .expert, которая может подсказать какой индекс стоит добавить для вводимого запроса.

Создание резервной копии Vacuum Into

С версии 3.27.0 команда vacuum расширена ключевым словом into, позволяющим создать копию базы без её остановки прямо из SQL. Является простой альтернативой Backup API.

vacuum into 'D:/backup/' || strftime('%Y-%M-%d', 'now') || '.sqlite';

Функция printf

Функция является аналогом С-функции. При этом NULL-значения интерпретируются как пустая строка для %s и 0 для плейсхолдера числа.

select 'a' || ' 123 ' || null; --> nullselect printf('%s %i %s', 'a', 123, null); --> 123 aselect printf('%s %i %i', 'a', 123, null); --> 123 a 0

Время и дата

В SQLite нет типов Date и Time. Хотя и можно создать таблицу с колонками таких типов, это будет аналогично созданию колонок без указания типа, поэтому данные в таких колонках хранятся как текст. Это удобно при просмотре данных, однако имеет ряд недостатков: неэффективный поиск, если нет индекса, данные занимают много места, отсутсвует временная зона. Для избежания этого можно хранить данные как unix-время, т.е. число секунд, прошедших с полуночи 01.01.1970.

select strftime('%Y-%M-%d %H:%m', 'now'); --> UTC времяselect strftime('%Y-%M-%d %H:%m', 'now', 'localtime'); --> местное времяselect strftime('%s', 'now'); -- текущее Unix-время select strftime('%s', 'now', '+2 day'); --> текущее unix-время плюс два дня-- Конвертация unix-времени в локальное для пользователя - 21-11-2020 15:25:14select strftime('%d-%m-%Y %H:%M:%S', 1605961514, 'unixepoch', 'localtime')

Json

С версии 3.9.0 в SQLite можно работать с json (требуется либо флаг SQLITE_ENABLE_JSON1 при компиляции или загруженное расширение). Данные json хранятся как текст. Результат функций также текст.

select json_array(1, 2, 3); --> [1,2,3] (строка)select json_array_length(json_array(1, 2, 3)); --> 3select json_array_length('[1,2,3]'); --> 3select json_object('a', json_array(2, 5), 'b', 10); --> {"a":[2,5],"b":10} (строка)select json_extract('{"a":[2,5],"b":10}', '$.a[0]');  --> 2select json_insert('{"a":[2,5]}', '$.c', 10); --> {"a":[2,5],"c":10} (строка)select value from json_each(json_array(2, 5)); --> 2 строки 2, 5select json_group_array(value) from json_each(json_array(2, 5)); --> [2,5] (строка)

Полнотекстовый поиск

Как и json, полнотекстовый поиск требует задания флага SQLITE_ENABLE_FTS5 при компиляции или загрузки расширения. Для работы с поиском, сперва создается виртуальная таблица с индексируемыми полями, а и потом туда загружаются данные, используя обычный insert. Следует иметь в виду, что для своей работы расширение создает дополнительные таблицы и созданная виртуальная таблица использует их данные.

create virtual table emails using fts5(sender, body);SELECT * FROM emails WHERE emails = 'fts5'; -- sender или body содержит fts5

Расширения

Возможности SQLite могут быть добавлены через загружаемые модули. Некоторые из них уже были упомянуты выше json1 и fts.

Расширения могут использоваться как для добавления пользовательских функций (не только скалярных, как, например, crc32, но и агрегирующих или даже оконных), так и виртуальных таблиц. Виртуальные таблицы это таблицы, которые присутствуют в базе, но их данные обрабатываются расширением, при этом, в зависимости от реализации, некоторые из них требуют создания

create virtual table temp.tab1 using csv(filename='thefile.csv');select * from tab1;

Другие же, так называемые table-valued, могут использоваться сразу

select value from generate_series(5, 100, 5);
.
Часть виртуальных таблиц перечислена здесь.

Одно расширение может реализовать как функции, так и виртуальные таблицы. Например, json1 содержит 13 скалярных и 2 агрегирующие функции и две виртуальные таблицы json_each и json_tree. Чтобы написать свою функцию достаточно иметь базовые знания С и разобрать код расширений из репозитария SQLite. Реализация своих виртуальных таблиц несколько сложнее (видимо поэтому их мало). Тут можно рекомендовать не сильно устаревшую книгу Using SQLite by Jay A. Kreibich, статью Michael Owens, шаблон из репозитария и код generate_series, как table-valued функции.

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

Разное

  • Используйте ' (одинарная кавычка) для строковых констант и " (двойная кавычка) для имен столбцов и таблиц.
  • Чтобы получить информацию по таблице tab1 можно использовать

    -- В main схемеselect * from pragma_table_info('tab1');-- В temp схеме или подключенной (attach) базеselect * from pragma_table_info('tab1') where schema = 'temp'
    
  • У SQLite есть свой официальный форум, где участвует и создатель SQLite Richard Hipp, и где можно оставить сообщение о баге.
  • Редакторы SQLite: SQLite Studio, DB Browser for SQLite и (реклама!) sqlite-gui (только Windows).
Подробнее..
Категории: Sql , Sqlite

Видеомонтаж, машинное обучение и взломанный xml все в одной программе

08.02.2021 14:10:06 | Автор: admin

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

В какой то момент пришла идея совместить работу с хобби, прочитал статью на хабре о распознавании объектов на картинках с помощью Core ML, с этого собственно все и началось. Поделюсь скромным опытом и проблемами с которыми можно столкнуться при разработке приложений работающих с Core ML.

Дело в том что почти треть работы видеомонтажера заключается в рутинном поиске видеоряда из исходников, которые надо каждый раз шерстить в поиске контекстного плана под закадровый текст, по моему это не несет никакой творческой составляющей, особенно когда ты занимаешься этим 15 лет). Ну и подумал я, а что если написать софтину, которая будет проходится по папке с исходниками, распознавать объекты, аккуратненько складывать их в БД. Далее, в момент поиска видео фрагментов для так называемой джинсы, вводится поисковое слово, например Солнце, и все что находится каким то образом передается в монтажную систему.

Идея зрела, собирался стёк, писать решил на Swift, обученные модели собственно Core ML, база данных SQLite. На первый взгляд идея казалась легко реализуемой, вроде ничего сложного.

Очень быстро накидал основной код, который вытаскивает кадры из видео, распознает обьекты с помощью модели Resnet50, которую рекомендовали яблочники у себя на сайте, она очень шустро работала и позволяла настраивать процент при котором считать объект распознанным. Сам код спокойно раздается на том же apple.com для всех желающих. Подключил библиотеку SQLite.swift, обернул ее функции в свои методы, все работает!

Потом еще пришлось неплохо повозиться с алгоритмами создания очереди обработки списка файлов и в этот момент я обратил внимание что программа то разрослась! Уже после 1000-й строчки кода вдруг пришло понимание что mvc-паттерн уже совсем не подходит для этого проекта, а именно он обычно и предлагается на всех туториалах и подсказках из Stackoverflow. Как же затягивает процесс когда все получается и даже не обращаешь внимания что у тебя весь код навален в одном файле. Стал раскидывать все по классам, синглтонам и прочим сущностям. Вроде стало полегче, но это не надолго, ибо впереди еще нужно распаралелить процессы на потоки, что бы программа не замирала пока идет процесс распознавания в большом количестве файлов.

Почитал статьи о многопоточных приложениях, о Grand Central Dispatch (GCD) - технологии Apple, предназначенная для многоядерных процессоров, вроде бы тоже все просто - кидаешь фоновую работу в основной поток а обновление интерфейса в главный поток и опять все работает! Но что то подсказывало что так легко и быстро не бывает! Начался процесс тестирования.

Первый серьезный глюк дал о себе знать когда запустил сканировать большой архив семейных видеофайлов, 70 гигов, видео снятые в разное время на разные телефоны и поэтому и разные форматы - идеально! Как раз то что надо для тестирования! Сканирование останавливалось на 420-ом файле, снятом на какой-то старый Самсунг под windows mobile, ну да ладно, может битый файл, подумал я и удалил его, запустил снова. опять 420 файл! Совершенно в другом формате, с яблофона, не битый! Что за магия такая? Ну давайте и его удалим. еще раз опять 420 файл пора лезть в дебаггер.

Две недели, две недели жизни (в свободное от работы время) я посвятил поиску этой ошибки! Виновником оказался объект VNCoreMLRequest, работающий с запросами к ML-модели и который не любит когда его используют в нескольких потоках, при этом он никак не проявляет себя в логах дебаггера а просто выдает ошибку времени выполнения, проще говоря кладет один из потоков. Так же порадовал метод обработки изображений copyCGImage, который отказывался работать стабильно, правда яблочники предупредили об этом на своем ресурсе для разработчиков и предлагали использовать вместо него другой асинхронный метод generateCGImagesAsynchronously, который как ни странно работал еще хуже, в итоге я вернулся к первому методу окружив его блоком try catch.

Следующим этапом проектирования стала идея распознавать обьекты сразу несколькими обученными моделями для более эффективного результата. То есть если одна модель в одном кадре ничего не нашла, то вторая тоже имея другой набор данных в своих нейронах может чего и найдет. Я создал три слота для подгрузки моделей: первые два для поставляемых с приложением моделей YOLOv3 и Resnet50, и третий слот для любой другой модели, которая может быть подгружена из файла. Я посчитал что кто то из пользователей заведет свои собственные модели для специфических задач и тогда третий слот будет очень кстати.

Слоты для CoreML моделей в настройках программыСлоты для CoreML моделей в настройках программы

К тому же Apple в поставке с Xcode теперь предлагает отдельный инструмент Create ML для создания своих моделей из набора картинок, там все очень просто, никаких командных строк, обычный пользовательский интерфейс для практически любого юсера.

Интерфейс программы Apple Create MLИнтерфейс программы Apple Create ML

Программа сформировывалась в завершенный продукт, не хватало одного - как пользователю выводить найденные видео фрагменты в программу видеомонтажа. Я наметил два варианта - это форматы EDL и XML. Реализовать первый формат не составляло особого труда, это старый известный с ленточных времен формат, используемый киношниками для переноса намеченных фрагментов в системы монтажа. Но проблема состояла в том, что EDL не содержит информацию о передаваемых файлах, а только о таймкодах, точках входа и выхода фрагмента, то есть в итоге пользователь получит набор фрагментов в секвенции, но они все будут оффлайн, потому что не известно из каких файлов брать эти куски, а ведь их, этих файлов может быть много, для каждого фрагмента свой файл. Другое дело XML! Он содержит всю информацию которую ты только можешь в него запихнуть: и путь, и формат файлов, и настройки звука, и даже применяемые маркеры, все что нужно, современный формат! Но вот реализовать всю эту крутизну это дело далеко не простое, и прочитать надо литературу которой нигде нет, ибо нужна информация именно по XML, используемом для экспорта секвенции именно с видео данными, а не какого нибудь там каталога для инет-магазина. Эту задачу я стал решать с изучения выведенного изAdobe Premiere шаблонной секвенции с парой файлов на таймлайне в XML. Полученный файл я открыл в текстовом редакторе и стал изучать. Постепенно стали вырисовываться блоки кода, для каждого плана на секвенции три блока - один для видео и два для звука, в общем теле кода сначала идут видео блоки а потом привязанные к ним аудио блоки, так же есть начальные и завершающее блоки файла с тегами описывающими, видимо, формат секвенции. Я разделил все эти блоки в отдельные файлы, которые обозначил как многострочные String ресурсы в Xcode. Создал отдельный класс, который оперирует этими блоками в цикле, собирая их в нужной последовательности в один код и подставляя в нужные места строковые данные с именем файла и информацией о таймкоде. Та еще работка! Хотя может быть абсолютно привычно для html-верстальщика.На первый взгляд сложная задача, но решена была довольно быстро, хотя это можно назвать хакерским методом) Но формат то по сути открытый! Другое дело что мы используем версию XML , сгенерированную Аdobe Premiere, с его тэгами, но насколько эти теги имеют проприетарный формат я рассуждать не берусь, знаю только что все работает, и в Final Cut Pro (в полной версии), и вдругих монтажках

Интерфейс программы VideoindexИнтерфейс программы Videoindex

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

Сейчас я думаю, чего еще можно добавить в приложение, есть уже некоторые идеи, и собственные и присланные пользователями, которые уже пользуются приложением. Например сейчас с появлением новых процессоров Apple Silicon, которые имеют аппаратное ускорение ML процессов до 16x, нужно обязательно сделать поддержку этой платформы в новых версиях. Ну а пока программа уже доступна в Mac App Store, называется Videoindex.

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

Подробнее..

Delphi и SQLite. Альтернатива хранимым процедурам

20.06.2021 14:08:21 | Автор: admin

SQLite во многих случаях является удобным, незаменимым инструментом. Я уже не могу себе представить - как мы все жили без него. Тем не менее, есть некоторые неудобства при его использовании, связанные с тем, что это легкая встраиваемая СУБД.

Самое большое неудобство для меня, как Delphi-разработчика - отсутствие хранимых процедур. Я очень не люблю смешивать Delphi-код и SQL-скрипты. Это делает код намного менее читабильным, и затрудняет его поддержку. Следовательно, нужно как-то разнести код Delphi и тексты SQL-скриптов.

Предлагаю свой вариант решения проблемы

  • Выносим весь SQL-код в отдельный тестовый файл ресурсов, подключенный к проекту.

  • Запросы в SQL-файле разделяем маркерами начала с идентификаторами и маркерами конца. В моём случае синтаксис маркера начала - //SQL ИмяПроцедуры. Маркер конца - GO.

  • Создаем класс - менеджер SQL-запросов. При загрузке приложения он читает SQL-файл из ресурсов и составляет из него список хранимых процедур с уникальными именами-идентификаторами.

  • В процессе работы приложения мендежер извлекает текст SQL-запроса по его идентификатору для последующей его передачи на выполнение.

Главная идея - простота и легкость использования, подобная вызову хранимых процедур и удобство при создании и модификации SQL-запросов

Код юнита менеджера запросов:

unit uSqlList;interfaceuses System.Classes, Winapi.Windows, System.SysUtils,  System.Generics.Collections;type  TSqlList = class(TObjectDictionary<string, TStrings>)  const    SCRIPTS_RCNAME = 'SqlList';  private    function GetScripts(const AName: string): TStrings;    procedure FillList;    function GetItem(const AKey: string): string;  public    constructor Create;  public    property Sql[const Key: string]: string read GetItem; default;  end;var  SqlList: TSqlList;implementationfunction GetStringResource(const AName: string): string;var  LResource: TResourceStream;begin  LResource := TResourceStream.Create(hInstance, AName, RT_RCDATA);  with TStringList.Create do    try      LoadFromStream(LResource);      Result := Text;    finally      Free;      LResource.Free;    end;end;{ TScriptList }constructor TSqlList.Create;begin  inherited Create([doOwnsValues]);  FillList;end;procedure TSqlList.FillList;var  LScripts: TStrings;  I: Integer;  S, LKey: string;  LStarted: Boolean;  LSql: TStrings;begin  LScripts := GetScripts(SCRIPTS_RCNAME);  try    LStarted := False;    LSql := nil;    for I := 0 to LScripts.Count - 1 do    begin      S := LScripts[I];      if LStarted then      begin        if S = 'GO' then        begin          LStarted := False;          Continue;        end        else if not S.StartsWith('//') then          LSql.Add(S);      end      else      begin        LStarted := S.StartsWith('//SQL ');        if LStarted then        begin          LKey := S.Substring(6);          LSql := TStringList.Create;          Add(LKey, LSql);        end;        Continue;      end;    end;  finally    LScripts.Free;  end;end;function TSqlList.GetItem(const AKey: string): string;begin  Result := Items[AKey].Text;end;function TSqlList.GetScripts(const AName: string): TStrings;begin  Result := TStringList.Create;  try    Result.Text := GetStringResource(AName);  except    FreeAndNil(Result);    raise;  end;end;initializationSqlList := TSqlList.Create;finalizationFreeAndNil(SqlList);end.

Пример содержимого файла SQL-скриптов:

//SQL GetOrderSELECT * FROM Orders WHERE ID = :IDGO//SQL DeleteOpenedOrdersDELETE FROM Orders WHERE Closed = 0GO

Подключение файла скриптов к проекту:

{$R 'SqlList.res' '..\Common\DataBase\SqlList.rc'}

Использование с компонентом TFDConnection:

  Connection.ExecSQL(SqlList['GetOrder'], ['123']);

Собственно, это всё. Использую данное решение уже в нескольких проектах и мне оно кажется очень удобным. Буду благодарен за советы и замечания. Рад, если мой посто кому-то будет полезен!

Подробнее..
Категории: Delphi , Sqlite , Sqlite3

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru