Создаём графики в приложении Flask с Chart.js

Создаём графики в приложении Flask с Chart.js
Я разрабатываю веб-сайты на Python и Flask, используя Bootstrap для стилизации. Недавно мне понадобилось добавить на сайт интерактивные графики. При выборе JavaScript библиотеки для этого, я наткнулся на Chart.js — у неё более 29 000 звёзд на GitHub (на май 2017). И сегодня мы рассмотрим три примера создания графиков с Chart.js в приложении Flask.

Исходные коды примеров из статьи вы можете найти в репозитории GitLab с отдельными тегами (example1, example2 и example3) для каждого примера: https://gitlab.com/patkennedy79/flask_chartjs_example

Структура приложения

.
├── README.md
├── app.py
├── requirements.txt
├── static
│ └── Chart.min.js
└── templates
├── chart.html
├── line_chart.html
└── time_chart.html

В папке templates хранятся следующие шаблоны:

  • Пример №1 – chart.html
  • Пример №2 – line_chart.html
  • Пример №3 – time_chart.html
Пример №1. Простой график

Мы сделаем небольшое приложение Flask, которое сгенерирует данные для их вывода с Chart.js. В файле app.py добавьте такие строки:

from flask import Flask
from flask import render_template
from datetime import time

app = Flask(__name__)

@app.route("/simple_chart")
def chart():
legend = 'Monthly Data'
labels = ["January", "February", "March", "April", "May", "June", "July", "August"]
values = [10, 9, 8, 7, 6, 4, 7, 8]
return render_template('chart.html', values=values, labels=labels, legend=legend)

if __name__ == "__main__":
app.run(debug=True)

Здесь мы создаём приложение Flask с единственным роутом (‘/simple_chart’), который обрабатывает файл chart.html. В шаблон chart.html передаётся набор значений для 8 месяцев года (только для примера).

Шаблон (включая Javascript)

Файл шаблона (chart.html) состоит из нескольких языков:

  • HTML
  • Шаблонные скрипты Jinja2
  • Javascript

Для работы Chart.js нужно вызвать файл ‘Chart.min.js’ в секции ‘head’:



Chart.js Example



Затем можно задать график как элемент канвы HTML5:

Simple Line Chart


The chart is displaying a simple line chart.


Секция Javascript должна выполнить следующие действия:

  1. Задать глобальные параметры для всех графиков
  2. Задать параметры для конкретного графика
  3. Получить элемент канвы HTML
  4. Создать график для вывода в канву

Ниже — содержимое секции ‘script’, в которой мы строим график с помощью Chart.js:

// Global parameters:
// do not resize the chart canvas when its container does (keep at 600x400px)
Chart.defaults.global.responsive = false;

// define the chart data
var chartData = {
labels : [{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} for item in labels {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}
"{{item}}",
{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} endfor {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}],
datasets : [{
label: '{{ legend }}',
fill: true,
lineTension: 0.1,
backgroundColor: "rgba(75,192,192,0.4)",
borderColor: "rgba(75,192,192,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(75,192,192,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(75,192,192,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
data : [{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} for item in values {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}
{{item}},
{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} endfor {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}],
spanGaps: false
}]
}

// get chart canvas
var ctx = document.getElementById("myChart").getContext("2d");

// create the chart using the chart canvas
var myChart = new Chart(ctx, {
type: 'line',
data: chartData,
});

Большинство параметров Chart.js взяты из документации к библиотеке, вы можете прочитать о них там.

Я лишь изменил параметр ‘responsive’ в глобальных настройках — рекомендую установить его в ‘false’, чтобы график не расползался на разных размерах монитора, а был бы около 600x400px (оптимально для ноутбуков и настольных компьютеров).

Запуск приложения

Для запуска приложения откройте консоль и перейдите в корень проекта. Затем выполните команду

$ python app.py
Затем откройте браузер и введите адрес http://localhost:5000/simple_chart

Вы должны увидеть следующее:

Создаём графики в приложении Flask с Chart.js
Превосходно! Мы сделали простое приложение, передающее данные в шаблон Chart.js.

Пример №2. Добавляем функции обратного вызова в диаграмму

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

В этом примере мы рассмотрим:

  1. Функция обновления заголовка выводимой точки графика
  2. Функция обновления определённой точки графика

Обновим приложение Flask (в нашем файле app.py) для добавления нового роута:

@app.route("/line_chart")
def line_chart():
legend = 'Temperatures'
temperatures = [73.7, 73.4, 73.8, 72.8, 68.7, 65.2,
61.8, 58.7, 58.2, 58.3, 60.5, 65.7,
70.2, 71.4, 71.2, 70.9, 71.3, 71.1]
times = ['12:00PM', '12:10PM', '12:20PM', '12:30PM', '12:40PM', '12:50PM',
'1:00PM', '1:10PM', '1:20PM', '1:30PM', '1:40PM', '1:50PM',
'2:00PM', '2:10PM', '2:20PM', '2:30PM', '2:40PM', '2:50PM']
return render_template('line_chart.html', values=temperatures, labels=times, legend=legend)

Этот роут создаёт более сложный набор данных для вывода в график.

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

Файл шаблона line_chart.html для этого примера построен на основе первого шаблона chart.html. Код Javascript также обновлён — добавлен обратный вызов для обновления заголовка при наведении пользователем курсора над точкой графика:

// create the chart using the chart canvas
var myChart = new Chart(ctx, {
type: 'line',
data: chartData,
options: {
tooltips: {
enabled: true,
mode: 'single',
callbacks: {
label: function(tooltipItems, data) {
return tooltipItems.yLabel + ' degrees';
}
}
},
}
});

В этой функции мы добавляем слово ‘degrees’ к значению температуры и выводим это в заголовок.

Добавляем обратный вызов для выбранной точки

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

// get chart canvas
var holder = document.getElementById("myChart");

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

// get the text element below the chart
var pointSelected = document.getElementById("pointSelected");

И, наконец, функцию обратного вызова:

// create a callback function for updating the selected index on the chart
holder.onclick = function(evt){
var activePoint = myChart.getElementAtEvent(evt);
pointSelected.innerHTML = 'Point selected... index: ' + activePoint[0]._index;
};

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

console.log(activePoint);
console.log('x:' + activePoint[0]._view.x);
console.log('maxWidth: ' + activePoint[0]._xScale.maxWidth);
console.log('y: ' + activePoint[0]._view.y);

Запускаем приложение

Запустим в консоли

$ python app.py
и откроем в браузере http://localhost:5000/line_chart

Создаём графики в приложении Flask с Chart.js

Пример №3. Временные графики

Большая проблема с предыдущим примером — то, что время в ней задано строкой. Лучшим решением является использование модуля time из Python и библиотеки Moment.js на стороне Javascript. Moment.js предоставляет удобные методы и структуры данных для управления временными значениями.

Мы создадим шаблон для третьего примера на базе предыдущего шаблона.

Добавьте в начало файла app.py импорт модуля:

from datetime import time
Далее создадим роут ‘/time_chart’, в котором зададим набор некоторых температурных значений и временные данные в формате hh:mm:ss

@app.route("/time_chart")
def time_chart():
legend = 'Temperatures'
temperatures = [73.7, 73.4, 73.8, 72.8, 68.7, 65.2,
61.8, 58.7, 58.2, 58.3, 60.5, 65.7,
70.2, 71.4, 71.2, 70.9, 71.3, 71.1]
times = [time(hour=11, minute=14, second=15),
time(hour=11, minute=14, second=30),
time(hour=11, minute=14, second=45),
time(hour=11, minute=15, second=00),
time(hour=11, minute=15, second=15),
time(hour=11, minute=15, second=30),
time(hour=11, minute=15, second=45),
time(hour=11, minute=16, second=00),
time(hour=11, minute=16, second=15),
time(hour=11, minute=16, second=30),
time(hour=11, minute=16, second=45),
time(hour=11, minute=17, second=00),
time(hour=11, minute=17, second=15),
time(hour=11, minute=17, second=30),
time(hour=11, minute=17, second=45),
time(hour=11, minute=18, second=00),
time(hour=11, minute=18, second=15),
time(hour=11, minute=18, second=30)]
return render_template('time_chart.html', values=temperatures, labels=times, legend=legend)

На стороне Javascript мы импортируем библиотеку Moment.js и будем строить временные значения по шкале x:



Chart.js Example




Обновим секцию ‘script’, добавив функцию конвертации временных значений в структуру для Moment.js

var timeFormat = 'hh:mm:ss';

function newDateString(hours, minutes, seconds) {
return moment().hour(hours).minute(minutes).second(seconds).format(timeFormat);
}

Эта функция будет обновлять текст на графике:

// define the chart data
var chartData = {
labels : [{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} for item in labels {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}
newDateString( {{item.hour}}, {{item.minute}}, {{item.second}} ),
{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} endfor {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}],

Запустим приложение:

$ python app.py
и откроем браузер по адресу http://localhost:5000/time_chart

Создаём графики в приложении Flask с Chart.js

По материалам «Creating Charts with Chart.js in a Flask Application»

Pytube: скачиваем Youtube видео с помощью Python

Pytube: скачиваем Youtube видео с помощью Python


Pytube это библиотека для скачивания видео с Youtube. Как сказано в документации, у неё нет сторонних зависимостей и она построена на стандартной библиотеке.

Установка

Установить пакет можно с помощью менеджера пакетов pip:

pip install pytube

Эта команда найдёт пакет в репозитории, скачает его и установит на локальную машину. Вывод команды будет примерно таким:

Collecting pytube
  Downloading pytube-6.2.2-py2-none-any.whl
Installing collected packages: pytube
Successfully installed pytube-6.2.2
Работаем с pytube

После завершения установки откройте консоль python и импортируйте пакет pytube:

import pytube

Класс YouTube представляет экземпляр YouTube-сессии, поэтому его нужно инициализировать так:

yt = pytube.YouTube('https://www.youtube.com/watch?v=xtZOnyYu16U')

После этого в переменной yt будет храниться вся информация о переданном видео, к примеру название файла:

fn = yt.filename

Как известно, чтобы получить значение переменной в консоли python, нужно ввести её наименование и нажить кнопку Ввод на клавиатуре:

fn
Вы должны увидеть следующее:

u'Stoto - Late Night (Original Mix)'

Всё довольно очевидно — вы видите имя файла от переданной ссылки на видео при создании экземпляра класса в его методе YouTube.__init__.

Также нам доступен атрибут video_id, который определяется при создании экземпляра класса. Можно вывести его так:

video_id = yt.video_id
print(video_id)
'xtZOnyYu16U'

Доступные для скачивания видео можно получить методом get_videos:

videos = yt.get_videos()

Выведем в цикле каждое из них:

for v in videos:
    print(v)

Для моего видео вывод будет таким:

<Video: MPEG-4 Visual (.3gp) - 144p - Simple>
<Video: MPEG-4 Visual (.3gp) - 240p - Simple>
<Video: H.264 (.mp4) - 360p - Baseline>
<Video: H.264 (.mp4) - 720p - High>
<Video: VP8 (.webm) - 360p - N/A>

Тип каждого объекта получим так:

for v in videos:
    print(type(v))

Вывод должен быть таким:

<class 'pytube.models.Video'>
<class 'pytube.models.Video'>
<class 'pytube.models.Video'>
<class 'pytube.models.Video'>
<class 'pytube.models.Video'>

Класс pytube.models.Video используется для создания отдельных экземпляров видео с дальнейшим скачиванием.

Первый экземпляр можно получить по его индексу:

first_video = videos[0]
print(first_video)

Video имеет много атрибутов, таких как filename, url, extension, video_codec, audio_codec, audio_bitrate, resolution и т.д.

Расширение видео получим так:

first_video.extension
'3gp'

Качество видео:

quality = first_video.resolution
print(quality)
'144p'

Битрейт:

audio_bitrate = first_video.audio_bitrate
video_bitrate = first_video.video_bitrate
print(audio_bitrate)
print(video_bitrate)

Скачать видео также просто:

first_video.download('/Users/oltjano/Desktop')

Выбранное видео будет загружено в папку /Users/oltjano/Desktop

А если вам нужно перезаписать загруженное ранее видео, присвойте переменной force_overwrite значение True:

first_video.download(path='/Users/oltjano/Desktop', force_overwrite=True)

Скачиваем видео определённого формата

Начнём с начала. Перезапустите консоль python и введите:

import pytube
video_link = 'https://www.youtube.com/watch?v=8ZpVwAeLzm4'

Создадим Youtube сессию и запросим доступные форматы:

yt = pytube.YouTube(video_link)
videos = yt.videos

Используя метод get можно отфильтровать видео по формату и качеству:

video = yt.get('mp4', '720p')
Если такое видео будет доступно, в переменной появятся ссылки на него, иначе — будет выведена ошибка.

Итак, у нас нет ошибки, значит, видео можно скачать. Зададим путь для загруженного файла и загрузим видео:

path = '/Users/Oltjano/Desktop'
video.download(path)

Готово!

Источник: «Pytube: How to Download Youtube Videos with this Python Tool»

Dear Diary — зашифрованный дневник в консоли на Python

Dear Diary - зашифрованный дневник в консоли на Python
Я уже писал о том, как работать с шифрованными базами данных на Python. И сегодня для закрепления материала мы напишем небольшую программу для хранения текстовых заметок в зашифрованном виде.

Для начала установим необходимые библиотеки. Шифрование возьмёт на себя SQLCipher — библиотека с открытым исходным кодом, которая шифрует базы SQLite алгоритмом AES-256 цепочками шифроблоков. Для этого установим соответствующий пакет:

$ pip install pysqlcipher
Также установим peewee для безопасной работы с записями базы данных:

$ pip install peewee

Работа с БД

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

#!/usr/bin/env python

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

db = SqlCipherDatabase(None)

class Entry(Model):
content = TextField()
timestamp = DateTimeField(default=datetime.datetime.now)

class Meta:
database = db

def initialize(passphrase):
db.init('diary.db', passphrase=passphrase, kdf_iter=64000)
Entry.create_table(fail_silently=True)

В стандартной библиотеке python есть модуль для чтения паролей из stdin без вывода пароля на экран. Мы воспользуемся этим модулем для безопасного ввода пароля при разблокировке дневника.

В начале работы приложения мы принимаем пароль (как ключ к шифру БД), инициализируем базу данных, а затем выводим главное меню (о нём будет ниже):

#!/usr/bin/env python

from getpass import getpass
import sys

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

# ... Здесь код модели из предыдущего блока ...

if __name__ == '__main__':
# безопасно принимаем пароль.
passphrase = getpass('Enter password: ')

if not passphrase:
sys.stderr.write('Passphrase required to access diary.\n')
sys.stderr.flush()
sys.exit(1)

# инициализируем базу данных.
initialize(passphrase)
menu_loop()

Интерактивное меню

Обычно в мире *nix не принято делать интерактивные меню, это излишне усложняет программу. Однако для дневника, думаю, это отличный подход — так можно будет ввести пароль лишь один раз, а потом выполнять много различных операций в ней уже без пароля.

Для простоты сделаем наше меню выполняющим всего три операции:

  • Добавление новой записи
  • Вывод списка записей, отсортированных по дате создания
  • Поиск записей, основываясь на поиске подстроки в тексте

Вот структура меню и его операции:

#!/usr/bin/env python

from collections import OrderedDict
import datetime
from getpass import getpass
import sys

from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase

# ... Здесь код модели из предыдущего блока ...

def menu_loop():
choice = None
while choice != 'q':
for key, value in menu.items():
print('{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s) {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s' {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} (key, value.__doc__))
choice = raw_input('Action: ').lower().strip()
if choice in menu:
menu[choice]()

def add_entry():
"""Добавление записи"""

def view_entries(search_query=None):
"""Вывод прошлых записей"""

def search_entries():
"""Поиск записей"""

menu = OrderedDict([
('a', add_entry),
('v', view_entries),
('s', search_entries),
])

if __name__ == '__main__':
# ... начало программы ...

Меню выводится после инициализации базы данных, предлагая выбор одного варианта из трёх. Программа завершает работу при нажатии q.

Создадим функцию add_entry. Она будет принимать строки, введённые пользователем, считывая их до получения EOF (Ctrl+d на моём компьютере). После ввода пользователем заметки, программа спросит о необходимости сохранить запись и вернётся в главное меню:

def add_entry():
"""Добавление записи"""
print('Enter your entry. Press ctrl+d when finished.')
data = sys.stdin.read().strip()
if data and raw_input('Save entry? [Yn] ') != 'n':
Entry.create(content=data)
print('Saved successfully.')

Вызов sys.stdin.read() автоматически читает до появления EOF. А так как menu_loop вызвал функцию, то при её завершении мы вернёмся обратно в главное меню.

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

Для повторного использования этой логики в дальнейшем, я написал функцию view_entries принимающей поисковый запрос:

def view_entries(search_query=None):
"""Вывод прошлых записей"""
query = Entry.select().order_by(Entry.timestamp.desc())
if search_query:
query = query.where(Entry.content.contains(search_query))

for entry in query:
timestamp = entry.timestamp.strftime('{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}A {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}B {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d, {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}Y {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}I:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}M{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}p')
print(timestamp)
print('=' * len(timestamp))
print(entry.content)
print('n) next entry')
print('q) return to main menu')
if raw_input('Choice? (Nq) ') == 'q':
break

А метод search_entries будет вызывать view_entries с передачей поискового запроса от пользователя. Вот его код:

def search_entries():
"""Поиск записей"""
view_entries(raw_input('Search query: '))

На этом наша программа завершена!

Что можно улучшить

Попробуйте самостоятельно реализовать следующий функционал:

  • Удаление записи
  • Умный постраничный вывод списка записей
  • Цвета в консоли для улучшения внешнего вида. Посмотрите в сторону Blessings для этого.
  • Редактирование записей
  • Календарный вид или поиск записей по дате
  • Веб-фронтенд?

Полный исходный код программы можно скачать здесь.

По материалам «Dear Diary, an Encrypted Command-Line Diary with Python»

Создаём Slack бота для отслеживания вашего бренда в Twitter

Создаём Slack бота для отслеживания вашего бренда в Twitter
Сегодня мы рассмотрим как отслеживать упоминания нашего домена в Twitter и постить об этом в канал Slack. Для этого отлично подойдут модули slacker и twython. Также мы используем configparser и logging в нашем проекте.

Настройки проекта

Мы будем использовать slacker и twython, поэтому установите их с помощью pip install или выполните команду pip install -r requirements.txt (после создания virtual env).

Вам понадобится токены от Twitter API и от Slack.

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

В случае со Slack вы должны сначала создать бота и добавить его в ваш канал.

Разработка

Полный код проекта находится здесь. Ниже я разберу его по шагам:

— импортируем настройки и токены:

import configparser
import logging

from slacker import Slacker
from twython import TwythonStreamer

config = configparser.ConfigParser()
config.read('config.ini')

CONSUMER_KEY = config['Twitter']['cs_key']
CONSUMER_SECRET = config['Twitter']['cs_secret']
ACCESS_TOKEN = config['Twitter']['acc_token']
ACCESS_SECRET = config['Twitter']['acc_secret']
SLACK_TOKEN = config['Slack']["token"]

— Зададим несколько констант. Помните, что CHANNEL должен содержать знак #, а домен для отслеживания должен быть задан как кортеж. Сообщение составлено так, чтобы можно было быстро перейти к твиту и понять кто его запостил:

CHANNEL = '#pybites-mentions'
DOMAIN = ('pybit', 'es')
MSG = '''A new mention of {domain}:

{user} (name: {name} / followers {followers}) tweeted:
{tweet_text}

Link to tweet: https://twitter.com/{user}/status/{tweet_id}
'''

— Создадим объект Slacker:

slack = Slacker(SLACK_TOKEN)
— настроим логгирование для возможной отладки в будущем:

logging.basicConfig(level=logging.DEBUG,
format='{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}(asctime)s {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}(name)-12s {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}(levelname)-8s {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}(message)s',
datefmt='{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}m-{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}H:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}M',
filename='bot.log')

— Метод создания сообщения в канале Slack:

def create_post(data):
tweet_text = data['text']
tweet_id = data['id_str']
user = data['user']['screen_name']
name = data['user']['name']
followers = data['user']['followers_count']
return MSG.format(domain='.'.join(DOMAIN),
user=user,
name=name,
followers=followers,
tweet_text=tweet_text,
tweet_id=tweet_id)

— Очень круто, что у нас есть Streaming API. TwythonStreamer позволяет очень легко начать мониторить Twitter: всего 6 строк кода в документации и 10 здесь, так как мы добавили обработку исключений, логгирование и постинг в Slack:

class MyStreamer(TwythonStreamer):
''' https://twython.readthedocs.io/en/latest/usage/streaming_api.html '''

def on_success(self, data):
post = create_post(data)
logging.debug(post)
try:
slack.chat.post_message(CHANNEL, post, as_user=True)
except Exception as exc:
logging.error('cannot post to channel: {}'.format(exc))

def on_error(self, status_code, data):
print('An error occurred: {}, exiting'.format(status_code))
self.disconnect()

— Приступим к Streamer. Внимание: pybit.es (одним словом) не работает, поэтому мы должны обрабатывать stream.statuses.filter ‘pybit es’, который работает как логическое И (подробнее об этом здесь):

if __name__ == "__main__":

stream = MyStreamer(CONSUMER_KEY, CONSUMER_SECRET,
ACCESS_TOKEN, ACCESS_SECRET)

# https://dev.twitter.com/streaming/overview/request-parameters#track
stream.statuses.filter(track=' '.join(DOMAIN))

Деплой

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

Результат

При любом упоминании нашего домена, мы получаем уведомление об этом:

Создаём Slack бота для отслеживания вашего бренда в Twitter

По материалам «How to Write a Simple Slack Bot to Monitor Your Brand on Twitter»

Создаём Slack бота на Python

Создаём Slack бота на Python
Недавно у меня был случай поработать с Real Time Messaging API в Slack и сегодня я покажу вам как создать своего бота — это очень просто!

Slack API

Немного о Slack Real Time Messaging API:

API сообщений реального времени (Real Time Messaging API) это API, построенное на WebSocket, позволяющее получать события из Slack в реальном времени и отправлять сообщения как от обычного пользователя. Его также называют «RTM API». Оно является базой для всех клиентов Slack. С его помощью создают ботов-помощников для вашей команды.

Подготовка

Прочитайте вначале об аккаунтах для ботов и затем создайте такой аккаунт. Вы получите API Token. Сохраните его в секрете! Свой токен я прописал в .bashrc, чтобы он не попал в систему хранения версий при разработке приложения. А получить его можно будет так:

slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN'))
Затем выполните команду установки необходимого модуля:

pip install slackclient и также у меня установлено несколько других модулей.

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

Теперь осталось получить BOT ID и также сохранить в скрипте логина (у меня он хранится в .bashrc):

$ python get_botid.py
Bot ID for 'pybitesbot' is XYZ

# .bashrc
export SLACK_BOT_TOKEN=ABC # первый этап
export BOT_ID=XYZ # получен из предыдущей команды
export WEATHER_API=123 # нужен будет для командных скриптов, об этом ниже

Действия бота

Я написал несколько скриптов, отвечающих на разные команды бота. Они лежат в папке commands проекта. Такая структура папок очень удобна для расширения списка команд в будущем.

В главном скрипте я загружаю все эти скрипты команд:

from commands.mood import get_mood # just a silly one
from commands.special import celebration
from commands.articles import get_num_posts
from commands.challenge import create_tweet
from commands.weather import get_weather # bot reports more sun and later sunset Spain vs Australia (sorry Julian haha)

# затем добавляем их в словарь COMMANDS
cmd_names = ('mood', 'celebration', 'num_posts', '100day_tweet', 'weather')
cmd_functions = (get_mood, celebration, get_num_posts, create_tweet, get_weather)
COMMANDS = dict(zip(cmd_names, cmd_functions))

Затем я переопределил (имеющуюся) функцию handle_command для того, чтобы бот отвечал на разные команды.

def handle_command(cmd, channel):

cmd = cmd.split()
cmd, args = cmd[0], cmd[1:]

if cmd in COMMANDS:
if args:
response = COMMANDS[cmd](*args)
else:
response = COMMANDS[cmd]()
else:
response = ('Not sure what you mean? '
'I can help you with these commands:\n'
'{}'.format('\n'.join(cmd_names)))

slack_client.api_call("chat.postMessage", channel=channel,
text=response, as_user=True)

Затем в main эта команды запускает цикл:

if slack_client.rtm_connect():
...

Деплой

Закончим на этом с кодом. На сервере я запускаю бота с помощью nohup чтобы он постоянно работал:

nohup python3 pybitesbot.py &
Обновлено: я нашёл маленькую ошибку, приводящую к непредвиденной остановке бота, поэтому написал небольшой скрипт для решения проблемы:

$ cat slackbot.sh
cmd="$HOME/bin/python3/bin/python3.5 pybitesbot.py"
until $cmd; do
echo "Slack bot crashed with exit code $?. Respawning.." >&2
sleep 1
done

$ ./slackbot.sh
StarterBot connected and running!

... нажимаем ctrl + c

^CTraceback (most recent call last):
File "pybitesbot.py", line 44, in
time.sleep(READ_WEBSOCKET_DELAY)
KeyboardInterrupt

Slack bot crashed with exit code 1. Respawning.. => thanks for the shell script
StarterBot connected and running!

# это была демонстрация, я всё ещё использую nohup для выхода из командной оболочки
$ nohup slackbot.sh &

Результат

Как видите, бот работает и с ним иногда очень весело:

Создаём Slack бота на Python

Что дальше?

Этот простейший пример с ботом заставил меня задуматься о том, как сделать по-настоящему умного бота для нашего сообщества. И почему бы не сделать бота, помогающего начинающим в решении их проблем с Python? Это было бы очень круто!

Я планирую написать продолжение этого примера, когда продвинусь немного в этом направлении…

Полный код доступен здесь. Форкайте его и начинайте разработку собственного крутого бота!

По материалам «How to Build a Simple Slack Bot»

Зашифрованные базы данных SQLite с Python и SQLCipher

Зашифрованные базы данных SQLite с Python и SQLCipher
SQLCipher это библиотека с открытым исходным кодом, созданная компанией Zetetic, для прозрачного 256-битного AES шифрования баз данных SQLite.
Сегодня мы рассмотрим пример написания программы на Python, взаимодействующей с зашифрованной базой SQLite. А пользователям peewee ORM я покажу использование расширения sqlcipher. Также мы рассмотрим конвертацию баз данных SQLite в формат зашифрованных баз для SQLCipher.

Сборка SQLCipher

Начнём с клонирования свежей версии SQLCipher и установки её в системе:

$ git clone https://github.com/sqlcipher/sqlcipher
$ cd sqlcipher

Чтобы скомпилировать SQLCipher, нам нужно прилинковать OpenSSL’s libcrypto, поэтому проверьте перед этим, что у вас установлен OpenSSL. Также я рекомендую установить модуль полнотекстового поиска. А здесь есть полный список опций компиляции SQLite.

$ ./configure \
--enable-tempstore=yes \
CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS" \
LDFLAGS="-lcrypto"

$ make
$ sudo make install

Теперь можно запустить оболочку sqlcipher, которая по-умолчанию подключена к in-memory базе данных.

$ sqlcipher
SQLCipher version 3.8.6 2014-08-15 11:46:33
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite>

Знакомимся с SQLCipher

Для создания шифрованной базы данных мы воспользуемся оболочкой SQLCipher, задав ей ключ с помощью команды PRAGMA:

sqlite> .open testing.db
sqlite> PRAGMA key='testing';
sqlite> create table people (name text primary key);
sqlite> insert into people (name) values ('charlie'), ('huey');
sqlite> .quit

Посмотрев на данные в testing.db, мы найдём их полностью искажёнными:

$ hexdump -C testing.db
0000 04 37 1e 64 12 fb a2 0b 8d 88 2f 72 fd c6 4b e6 |.7.d....../r..K.|
0010 7f 80 14 ec 74 68 83 00 e9 d2 4f 2e 80 5d 05 da |....th....O..]..|
0020 f0 44 f3 83 23 5e 29 e4 73 fc 29 1b 2d 6a 1d bc |.D..#^).s.).-j..|
0030 be 94 e6 12 6e 7a 28 32 15 cd 7b 1e a5 3c f7 52 |....nz(2..{..<.R| 0040 1a 51 37 40 28 70 3e fe 5d d9 0f 06 cc 76 4c 98 |.Q7@(p>.]....vL.|
...

Открыв эту базу данных обычным клиентом SQLite или задав некорректный ключ, мы получим ошибку:

$ sqlite3 testing.db
SQLite version 3.8.7 2014-10-17 11:24:17
Enter ".help" for usage hints.
sqlite> .schema
Error: file is encrypted or is not a database
sqlite> .quit

$ sqlcipher testing.db
SQLCipher version 3.8.6 2014-08-15 11:46:33
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> pragma key='wrong';
sqlite> .schema
Error: file is encrypted or is not a database

SQLCipher также имеет множество команд кроме PRAGMA, полный список доступен здесь.

Сборка pysqlcipher

Выполните следующие команды для установки последней версии pysqlcipher:

$ git clone https://github.com/leapcode/pysqlcipher/
$ cd pysqlcipher
$ python setup.py build_sqlcipher # Build against the system libsqlcipher
$ sudo python setup.py install

Не устанавливайте pysqlcipher с помощью pip, в результате этого он будет установлен из другого места. А для того, чтобы установить pysqlcipher в virtualenv, необходимо склонировать исходники в virtualenv и там их собрать:

$ cd my_env && source bin/activate
$ git clone https://github.com/leapcode/pysqlcipher/
$ cd pysqlcipher
$ python setup.py build_sqlcipher
$ python setup.py install

Работаем с шифрованными базами из Python

>>> from pysqlcipher import dbapi2 as sqlcipher
>>> db = sqlcipher.connect('testing.db')

Для того, чтобы делать запросы к базе данных, необходимо задать ключ с помощью PRAGMA, и дополнительно задать итерации деривации ключа с помощью PRAGMA kdf_iter, который по-умолчанию имеет значение 64000.

>>> db.executescript('pragma key="testing"; pragma kdf_iter=64000;') >>> db.execute('select * from people;').fetchall()
[(u'charlie',), (u'heuy',)]

При коннекте с некорректным паролем мы получим ошибку DatabaseError:

>>> db = sqlcipher.connect('testing.db')
>>> db.execute('pragma key="wrong"') >>> db.execute('select * from people;')
Traceback (most recent call last):
File "", line 1, in
pysqlcipher.dbapi2.DatabaseError: file is encrypted or is not a database

Используем SQLCipher с Peewee ORM

Если у вас ещё не установлен peewee, самое время сделать это:

$ pip install peewee
Расширение peewee SQLCipher позволяет работать с шифрованными базами. Чтобы создать, к примеру, зашифрованный дневник, напишем следующий код:

import datetime

from playhouse.sqlcipher_ext import *

db = SqlCipherDatabase('diary.db', passphrase='my secret passphrase')

class Note(Model):
content = TextField()
timestamp = DateTimeField(default=datetime.datetime.now)

class Meta:
database = db

Если этот код — код модели в файле diary.py, то мы можем работать с ним в командной строке таким образом:

>>> from diary import Note
>>> Note.create_table(fail_silently=True)
>>> Note.create(content='Dear diary, today I had a good day!')
>>> Note.create(content='Dear diary, huey threw up on the floor.')
>>> for note in Note.select():
... print note.timestamp, note.content
...
2014-10-27 21:05:58.488291 Dear diary, today I had a good day!
2014-10-27 21:06:16.663230 Dear diary, huey threw up on the floor.

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

import datetime
import getpass

from playhouse.sqlcipher_ext import *

db = SqlCipherDatabase(None) # Defer initialization of the database.

class Note(Model):
content = TextField()
timestamp = DateTimeField(default=datetime.datetime.now)

class Meta:
database = db

passphrase = getpass.getpass('Enter the diary password: ')
db.init('cipher.db', passphrase=passphrase)

Также вы можете воспользоваться вместо запроса ключа переменными среды или взять библиотеку python-keyring.

Шифруем имеющуюся базу SQLite

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

$ sqlcipher plaintext.db
sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'my password';
sqlite> SELECT sqlcipher_export('encrypted');
sqlite> DETACH DATABASE encrypted;

Вот и всё! Теперь в encrypted.db будет храниться зашифрованная копия данных из plaintext.db.

По материалам: «Encrypted SQLite Databases with Python and SQLCipher»

Создание PDF в Django с помощью WeasyPrint

Создание PDF в Django с помощью WeasyPrint
В создании pdf файлов в Django очень помогает библиотека WeasyPrint. Сегодня мы рассмотрим пример работы с ней.

WeasyPrint это движок для обработки HTML и CSS, которые выгружаются в формат PDF. Он поддерживает современные стандарты печати, бесплатный и распространяется под лицензией BSD.

Установка

pip install WeasyPrint
Для демонстрации возможностей библиотеки, создадим модель people с тремя полями (name, surname и email). А затем мы выведем документ pdf с таблицей, в которой выведем данные всех people с их данными. Приступим!

1. Создадим простенькое представление для вывода и создания http ответа в views.py:

# -*- coding: utf-8 -*-
from .models import Person
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML
import tempfile

def generate_pdf(request):
"""Создание pdf."""
# Данные модели
people = Person.objects.all().order_by('last_name')

# Обработка шаблона
html_string = render_to_string('bedjango/pdf.html', {'people': people})
html = HTML(string=html_string)
result = html.write_pdf()

# Создание http ответа
response = HttpResponse(content_type='application/pdf;')
response['Content-Disposition'] = 'inline; filename=list_people.pdf'
response['Content-Transfer-Encoding'] = 'binary'
with tempfile.NamedTemporaryFile(delete=True) as output:
output.write(result)
output.flush()
output = open(output.name, 'r')
response.write(output.read())

return response
2. Добавим новый url в urls.py для привязки нового представления, которое сгенерирует pdf:

from django.conf.urls import url
from . import views

urlpatterns = [
url(r'^generate/pdf/$', views.generate_pdf, name='generate_pdf'),
]

3. Теперь создадим html шаблон, чтобы передать разметку и оформление в pdf. Большой плюс этой библиотеки как раз в том, что мы можем использовать html и css:



Test


List of people

{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} for person in people {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}

{{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} endfor {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}

Last Name First Name Email
{{ person.last_name }} {{ person.first_name }} {{ person.email }}



4. Мы можем использовать теги django ({{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} for {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}}, {{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} if {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}} и т.д.) в шаблонах. Проверим что получилось:

Создание PDF в Django с помощью WeasyPrint
Как видите, всего за четыре простых шага мы научили django выводить информацию в виде PDF.

По материалам «How To Generate PDF in Django with WeasyPrint»

Как мы автоматизировали ежедневный твит проекта 100DaysOfCode

Как мы автоматизировали ежедневный твит проекта 100DaysOfCode
В этом посте я покажу вам способ сделать автоматический твит о прогрессе в испытании #100DaysOfCode Challenge. После этого у вас останется больше времени на разработку. Супер, да?

Это день 007 нашего испытания 100 Days of Code. Весь код проекта хранится здесь.

Подготовка

Итак, нам понадобятся pytz, tweepy и requests. Вы можете установить их все разом командой:

pip install -r requirements.txt если вы склонировали репозиторий. Также рекомендую использовать virtualenv для изоляции окружений.

Вам также понадобятся Consumer Key/Secret и Access Token (Secret) из Twitter. Я добавил их в .bashrc, который я загружаю через os.environ в config.py. Там же запускается обработчик логгирования, записывающий исходящие твиты и все возможные ошибки.

Главный скрипт

Согласно PEP8, вначале импортируем стандартную библиотеку, затем внешние модули, а ниже — модули проекта:

import datetime
import os
import re
import sys

import requests
import pytz

from config import logging, api
Мой сервер запущен в часовом поясе Mountain Time, а мне необходимо было работать с поясами в EMEA. В этом нам поможет pytz, с ним очень легко работать с любыми часовыми поясами:

tz = pytz.timezone('Europe/Amsterdam')
now = datetime.datetime.now(tz)
start = datetime.datetime(2017, 3, 29, tzinfo=tz) # = PyBites 100 days :)

По PEP8 константы зададим символами в верхнем регистре с разделителем в виде подчёркивания. Очень удобный расчёт дат:

CURRENT_CHALLENGE_DAY = str((now - start).days).zfill(3)
LOG = 'https://raw.githubusercontent.com/pybites/100DaysOfCode/master/LOG.md'
LOG_ENTRY = re.compile(r'\[(?P.*?)\]\((?P<day>\d+)\)')<br /> REPO_URL = 'https://github.com/pybites/100DaysOfCode/tree/master/'<br /> TWEET_LEN = 140<br /> TWEET_LINK_LEN = 23</code><br /> Ну и куда без requests? Так одной строкой я получаю файл LOG.md из <a href="https://github.com/pybites/100DaysOfCode" target="_blank">репозитория</a>:</p> <p><code>def get_log():<br /> return requests.get(LOG).text.split('\n')</code><br /> Получим название скрипта и строку с соответствующей датой из LOG.md (сегодня = ‘007’):</p> <p><code>def get_day_progress(html):<br /> lines = [line.strip()<br /> for line in html<br /> if line.strip()]</p> <p> for line in lines:<br /> day_entry = line.strip('|').split('|')[0].strip()<br /> if day_entry == CURRENT_CHALLENGE_DAY:<br /> return LOG_ENTRY.search(line).groupdict()</code><br /> Создаём твит. Я добавил немного кода для сокращения названия скрипт, если он превышает допустимый размер твита:</p> <p><code>def create_tweet(m):<br /> ht1, ht2 = '#100DaysOfCode', '#Python'<br /> title = m['title']<br /> day = m['day']<br /> url = REPO_URL + day<br /> allowed_len = TWEET_LEN + len(url) - TWEET_LINK_LEN</p> <p> fmt = '{} - Day {}: {} {} {}'<br /> tweet = fmt.format(ht1, day, title, url, ht2)<br /> surplus = len(tweet) - allowed_len</p> <p> if surplus > 0:<br /> new_title = title[:-(surplus + 4)] + '...'<br /> tweet = tweet.replace(title, new_title)<br /> return tweet</code><br /> Метод tweet_status() отправляет твит. Здесь мы используем импортированный объект api (из config.py) для отправки твита, а также запишем в лог информацию об успешной отправке или об ошибке:</p> <p><code>def tweet_status(tweet):<br /> try:<br /> api.update_status(tweet)<br /> logging.info('Posted to Twitter')<br /> except Exception as exc:<br /> logging.error('Error posting to Twitter: {}'.format(exc))</code><br /> Будем запускать наш скрипт из main. Также я добавил несколько переменных для проверок:</p> <p><code>if __name__ == '__main__':<br /> import socket<br /> local = 'MacBook' in socket.gethostname()<br /> test = local or 'dry' in sys.argv[1:]</code><br /> В режиме тестирования я использую локальный файл LOG:</p> <p><code> if test:<br /> log = os.path.basename(LOG)<br /> with open(log) as f:<br /> html = f.readlines()<br /> else:<br /> html = get_log()</code><br /> Если по какой-то причине я не смогу получить данные из get_day_progress(), скрипт прекратит работу и в лог запишется ошибка:</p> <p><code> m = get_day_progress(html)<br /> if not m:<br /> logging.error('Error getting day progress from log')<br /> sys.exit(1)</code><br /> Создаём твит. В режиме тестирования просто запишем его в лог, иначе — отправляем:</p> <p><code> tweet = create_tweet(m)<br /> if test:<br /> logging.info('Test: tweet to send: {}'.format(tweet))<br /> else:<br /> tweet_status(tweet)</code></p> <h6>Деплой</h6> <p>Есть несколько вещей, которые необходимо сделать для работы нашей программы: source .bashrc для загрузки переменных среды, экспортировать PYTHONPATH, задать полный путь до python3. И как <a href="https://unix.stackexchange.com/questions/27289/how-can-i-run-a-cron-command-with-existing-environmental-variables/27291#27291" target="_blank">сказано здесь</a>: «Cron ничего не знает о вашей оболочке; он запускается системой, поэтому у него минимум данных о среде.»</p> <p><code>$ crontab -l<br /> ...<br /> 34 14 * * * source $HOME/.bashrc && export PYTHONPATH=$HOME/bin/python3/lib/python3.5/site-packages && cd $HOME/code/100days/007 && $HOME/bin/python3/bin/python3.5 100day_autotweet.py</code></p> <h6>Результат</h6> <p>Какое совпадение: твит о прогрессе за сегодня как раз ушёл 🙂</p> <p><img title="Как мы автоматизировали ежедневный твит проекта 100DaysOfCode" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/04/21/a7ac1c02ac.png" class="image-center" alt="Как мы автоматизировали ежедневный твит проекта 100DaysOfCode" /></p> <h6>Логгирование</h6> <p>Очень полезная фишка модуля логгирования — автоматическое получение лога всех внешних модулей. Посмотрите в лог, там намного больше, чем пишет моя программа:</p> <p><code>$ vi 100day_autotweet.log<br /> ...<br /> ...<br /> 14:34:02 tweepy.binder INFO PARAMS: {'status': b'#100DaysOfCode - Day 007: script to automatically tweet 100DayOfCode progress tweet https://github.com/pybites/100DaysOfCode/tree/master/007 #Python'}<br /> ...<br /> many more log entries ...<br /> ...<br /> 14:34:02 requests.packages.urllib3.connectionpool DEBUG https://api.twitter.com:443 "POST /1.1/statuses/update.json?status={33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}23100DaysOfCode+-+Day+007{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}3A+script+to+automatically+tweet+100DayOfCode+progress+tweet+https{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}3A{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2F{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2Fgithub.com{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2Fpybites{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2F100DaysOfCode{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2Ftree{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2Fmaster{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}2F007+{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}23Python HTTP/1.1" 200 2693<br /> 14:34:02 root INFO Posted to Twitter ==> my message</code><br /> Конечно, это можно отключить, повысив уровень логгирования (INFO или ещё выше) в logging.basicConfig (в config.py). Ну и почитайте <a href="https://docs.python.org/3/library/logging.html" target="_blank">документацию</a> про это.</p> <p>По материалам «How we Automated our 100DaysOfCode Daily Tweet».</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"> <div class="saboxplugin-gravatar"><img title="Как мы автоматизировали ежедневный твит проекта 100DaysOfCode" alt="Как мы автоматизировали ежедневный твит проекта 100DaysOfCode" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div> <div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div> <div class="saboxplugin-desc"> <div itemprop="description"> <p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div> </div> <div class="clearfix"></div> <div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div> </div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/kak-my-avtomatizirovali-ezhednevnyj-tvit-proekta-100daysofcode.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-501" class="post-501 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/generirovanie-dokumentov-odt-s-secretary-v-django.html" rel="bookmark">Генерирование документов ODT с Secretary в Django</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/generirovanie-dokumentov-odt-s-secretary-v-django.html" title="10:45" rel="bookmark"><time class="updated" datetime="2019-01-04T17:27:58+03:00" itemprop="dateModified">04.01.2019</time><time class="entry-date published" datetime="2017-04-21T10:45:06+03:00" itemprop="datePublished">21.04.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Генерирование документов ODT с Secretary в Django" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/04/21/eb22ab0848.png" class="image-center" alt="Генерирование документов ODT с Secretary в Django" /><br /> Иногда бывает необходимость в создании odt для представления информации из приложения Django. Для этого случая можно использовать библиотеку <a href="https://github.com/christopher-ramirez/secretary" target="_blank">Secretary</a>.<br /> <cut></p> <p>Secretary это библиотека, позволяющая использовать формат ODT для представления информации в django. Secretary использует <a href="http://jinja.pocoo.org/docs/2.9/templates/" target="_blank">семантику шаблонов jinja2</a> для генерации документов.</p> <p>Установка Secretary:</p> <p><code>pip install secretary</code><br /> Для демонстрации возможностей библиотеки, создадим модель people с тремя полями (name, surname и email). А затем мы создадим документ с таблицей, в которой выведем данные всех people с их данными. Приступим!</p> <p>1. Добавим новый url в urls.py для привязки нового представления, которое сгенерирует документ.</p> <p><code>from django.conf.urls import url<br /> from . import views</p> <p>urlpatterns = [<br /> url(r'^generate/document/$', views.generate_document,name='generate_document')<br /> ]</code><br /> 2. Создадим простенькое представление для вывода и создания http ответа в views.py. Для упрощения логики в представлении, создадим класс со статическим методом, выводящим документ и создающим HTTP ответ.</p> <p><code># views.py<br /> from .models import Person<br /> from .logic import ReportGenerator</p> <p>def generate_document(request):<br /> # Model data<br /> people = Person.objects.all().order_by('last_name')</p> <p> return ReportGenerator().create_report(people)</code><br /> <code># logic.py<br /> from secretary import Renderer<br /> from django.http import HttpResponse<br /> import os, tempfile</p> <p>class ReportGenerator():<br /> """ Class ReportGenerator """</p> <p> @staticmethod<br /> def create_report(data):<br /> engine = Renderer()<br /> root = os.path.dirname(__file__)<br /> document = root + '/templates/bedjango/template.odt'<br /> result = engine.render(document, data=data)</p> <p> response = HttpResponse(content_type='application/vnd.oasis.opendocument.text; charset=UTF-8')<br /> response['Content-Disposition'] = 'inline; filename=people.odt'<br /> with tempfile.NamedTemporaryFile() as output:<br /> output.write(result)<br /> output.flush()<br /> output = open(output.name, 'r')<br /> response.write(output.read())</p> <p> return response</code><br /> 3. Создадим odt документ, который будем использовать как шаблон, для вывода информации из нашего приложения.</p> <p><img title="Генерирование документов ODT с Secretary в Django" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/04/21/8a87096631.png" class="image-center" alt="Генерирование документов ODT с Secretary в Django" /><br /> 4. Проверим результат.</p> <p><img title="Генерирование документов ODT с Secretary в Django" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/04/21/4faac1dd11.png" class="image-center" alt="Генерирование документов ODT с Secretary в Django" /><br /> Как видите, это было очень просто!</p> <p>Ещё раз ссылка на библиотеку: https://github.com/christopher-ramirez/secretary</p> <p>По материалам: «How To Generate Documents ODT With Secretary»</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"> <div class="saboxplugin-gravatar"><img title="Генерирование документов ODT с Secretary в Django" alt="Генерирование документов ODT с Secretary в Django" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div> <div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div> <div class="saboxplugin-desc"> <div itemprop="description"> <p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div> </div> <div class="clearfix"></div> <div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div> </div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/generirovanie-dokumentov-odt-s-secretary-v-django.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <article id="post-493" class="post-493 post type-post status-publish format-standard hentry category-development" itemtype='https://schema.org/CreativeWork' itemscope='itemscope'> <div class="inside-article"> <div class="article-holder"> <header class="entry-header"> <h2 class="entry-title" itemprop="headline"><a href="https://tehnojam.ru/category/development/go-mnogopotochnaja-zapis-v-fajl-csv.html" rel="bookmark">Go: многопоточная запись в файл CSV</a></h2> <div class="entry-meta"> <span class="posted-on"><a href="https://tehnojam.ru/category/development/go-mnogopotochnaja-zapis-v-fajl-csv.html" title="12:30" rel="bookmark"><time class="updated" datetime="2019-01-04T17:27:57+03:00" itemprop="dateModified">04.01.2019</time><time class="entry-date published" datetime="2017-04-18T12:30:25+03:00" itemprop="datePublished">18.04.2017</time></a></span> <span class="byline"><span class="author vcard" itemtype="https://schema.org/Person" itemscope="itemscope" itemprop="author">by <a class="url fn n" href="https://tehnojam.ru/category/author/fokusov" title="View all posts by Фокусов Игорь" rel="author" itemprop="url"><span class="author-name" itemprop="name">Фокусов Игорь</span></a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content" itemprop="text"> <p><img title="Go: многопоточная запись в файл CSV" decoding="async" src="http://tehnojam.ru/uploads/images/00/00/24/2017/04/18/1dfa8a.png" class="image-center" alt="Go: многопоточная запись в файл CSV" /><br /> Иногда при разработке приложений на Go бывает необходимость записи в файл CSV из нескольких горутин, при этом встроенный CSV Writer непотокобезопасен.<br /> <cut><br /> Моя первая попытка записи в файл CSV выглядела так:</p> <p><code>package main</p> <p>import (<br /> "encoding/csv"<br /> "os"<br /> "log"<br /> "strconv"<br /> )</p> <p>func main() {</p> <p> csvFile, err := os.Create("/tmp/foo.csv")<br /> if err != nil {<br /> log.Panic(err)<br /> }</p> <p> w := csv.NewWriter(csvFile)<br /> w.Write([]string{"id1","id2","id3"})</p> <p> count := 100<br /> done := make(chan bool, count)</p> <p> for i := 0; i < count; i++ { go func(i int) { w.Write([]string {strconv.Itoa(i), strconv.Itoa(i), strconv.Itoa(i)}) done <- true }(i) } for i:=0; i < count; i++ { <- done } w.Flush() }</code><br /> Этот сценарий должен выводить числа от 0 до 99 по три на строку. Некоторые строки записались правильно, но как мы видим, некоторые неправильно:</p> <p><code>40,40,40<br /> 37,37,37<br /> 38,38,38<br /> 18,18,39<br /> ^@,39,39<br /> ...<br /> 67,67,70,^@70,70<br /> 65,65,65<br /> 73,73,73<br /> 66,66,66<br /> 72,72,72<br /> 75,74,75,74,75<br /> 74<br /> 7779^@,79,77<br /> ...</code><br /> Есть способ, которым мы можем сделать наш код безопасным - использовать мьютекс при вызове методов CSV Writer-а. И я написал такой код:</p> <p><code>type CsvWriter struct {<br /> mutex *sync.Mutex<br /> csvWriter *csv.Writer<br /> }</p> <p>func NewCsvWriter(fileName string) (*CsvWriter, error) {<br /> csvFile, err := os.Create(fileName)<br /> if err != nil {<br /> return nil, err<br /> }<br /> w := csv.NewWriter(csvFile)<br /> return &CsvWriter{csvWriter:w, mutex: &sync.Mutex{}}, nil<br /> }</p> <p>func (w *CsvWriter) Write(row []string) {<br /> w.mutex.Lock()<br /> w.csvWriter.Write(row)<br /> w.mutex.Unlock()<br /> }</p> <p>func (w *CsvWriter) Flush() {<br /> w.mutex.Lock()<br /> w.csvWriter.Flush()<br /> w.mutex.Unlock()<br /> }</code><br /> Мы создаём мьютекс когда NewCsvWriter создаёт экземпляры CsvWriter, затем используем его в функциях Write и Flush, таким образом только одна горутина одновременно имеет доступ к базовому CsvWriter-у. Немного изменим наш код так, чтобы не вызывать CsvWriter напрямую:</p> <p><code>func main() {<br /> w, err := NewCsvWriter("/tmp/foo-safe.csv")<br /> if err != nil {<br /> log.Panic(err)<br /> }</p> <p> w.Write([]string{"id1","id2","id3"})</p> <p> count := 100<br /> done := make(chan bool, count)</p> <p> for i := 0; i < count; i++ { go func(i int) { w.Write([]string {strconv.Itoa(i), strconv.Itoa(i), strconv.Itoa(i)}) done <- true }(i) } for i:=0; i < count; i++ { <- done } w.Flush() }</code><br /> Сейчас если мы посмотрим в файл CSV, то увидим, что все строки корректны:</p> <p><code>...<br /> 25,25,25<br /> 13,13,13<br /> 29,29,29<br /> 32,32,32<br /> 26,26,26<br /> 30,30,30<br /> 27,27,27<br /> 31,31,31<br /> 28,28,28<br /> 34,34,34<br /> 35,35,35<br /> 33,33,33<br /> 37,37,37<br /> 36,36,36<br /> ...</code></p> <p>Источник: "Go: Multi-threaded writing to a CSV file"</p> <div class="saboxplugin-wrap" itemtype="http://schema.org/Person" itemscope itemprop="author"> <div class="saboxplugin-gravatar"><img title="Go: многопоточная запись в файл CSV" alt="Go: многопоточная запись в файл CSV" alt='' src='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=100&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3c9a724edc3d5745342294aed20872a8?s=200&d=mm&r=g 2x' class='avatar avatar-100 photo' height='100' width='100' itemprop="image"/></div> <div class="saboxplugin-authorname"><a href="https://tehnojam.ru/category/author/fokusov" class="vcard author" rel="author" itemprop="url"><span class="fn" itemprop="name">Фокусов Игорь</span></a></div> <div class="saboxplugin-desc"> <div itemprop="description"> <p>Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.</p> <p>Пишите: <a href="https://t.me/ighar">@ighar</a>. <a href="https://money.yandex.ru/to/410011020365993">Buy me a coffee, please</a> :).</p> </div> </div> <div class="clearfix"></div> <div class="saboxplugin-socials "><a target="_self" href="mailto:igor@fokusov.com" rel="nofollow" class="saboxplugin-icon-grey"><svg aria-hidden="true" class="sab-user_email" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M502.3 190.8c3.9-3.1 9.7-.2 9.7 4.7V400c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V195.6c0-5 5.7-7.8 9.7-4.7 22.4 17.4 52.1 39.5 154.1 113.6 21.1 15.4 56.7 47.8 92.2 47.6 35.7.3 72-32.8 92.3-47.6 102-74.1 131.6-96.3 154-113.7zM256 320c23.2.4 56.6-29.2 73.4-41.4 132.7-96.3 142.8-104.7 173.4-128.7 5.8-4.5 9.2-11.5 9.2-18.9v-19c0-26.5-21.5-48-48-48H48C21.5 64 0 85.5 0 112v19c0 7.4 3.4 14.3 9.2 18.9 30.6 23.9 40.7 32.4 173.4 128.7 16.8 12.2 50.2 41.8 73.4 41.4z"></path></svg></span></a></div> </div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"><span class="screen-reader-text">Categories </span><a href="https://tehnojam.ru/category/category/development" rel="category tag">Разработка</a></span><span class="comments-link"><a href="https://tehnojam.ru/category/development/go-mnogopotochnaja-zapis-v-fajl-csv.html#respond">Leave a comment</a></span> </footer><!-- .entry-meta --> </div> </div><!-- .inside-article --> </article><!-- #post-## --> <nav id="nav-below" class="paging-navigation"> <span class="screen-reader-text">Post navigation</span> <div class="nav-previous"><span class="prev" title="Previous"><a href="https://tehnojam.ru/category/author/fokusov/page/5" >Older posts</a></span></div> <div class="nav-next"><span class="next" title="Next"><a href="https://tehnojam.ru/category/author/fokusov/page/3" >Newer posts</a></span></div> <div class="nav-links"><a class="prev page-numbers" href="https://tehnojam.ru/category/author/fokusov/page/3">← Previous</a> <a class="page-numbers" href="https://tehnojam.ru/category/author/fokusov/">1</a> <span class="page-numbers dots">…</span> <a class="page-numbers" href="https://tehnojam.ru/category/author/fokusov/page/3">3</a> <span aria-current="page" class="page-numbers current">4</span> <a class="page-numbers" href="https://tehnojam.ru/category/author/fokusov/page/5">5</a> <span class="page-numbers dots">…</span> <a class="page-numbers" href="https://tehnojam.ru/category/author/fokusov/page/10">10</a> <a class="next page-numbers" href="https://tehnojam.ru/category/author/fokusov/page/5">Next →</a></div> </nav><!-- #nav-below --> </main><!-- #main --> </div><!-- #primary --> <div id="right-sidebar" itemtype="https://schema.org/WPSideBar" itemscope="itemscope" class="widget-area grid-25 tablet-grid-25 grid-parent sidebar"> <div class="inside-right-sidebar"> <aside id="categories-2" class="widget inner-padding widget_categories"><h2 class="widget-title">Категории</h2> <ul> <li class="cat-item cat-item-113"><a href="https://tehnojam.ru/category/category/it-comics">IT Комиксы</a> </li> <li class="cat-item cat-item-1"><a href="https://tehnojam.ru/category/category/%d0%b1%d0%b5%d0%b7-%d1%80%d1%83%d0%b1%d1%80%d0%b8%d0%ba%d0%b8">Без рубрики</a> </li> <li class="cat-item cat-item-12"><a href="https://tehnojam.ru/category/category/secure">Безопасность</a> </li> <li class="cat-item cat-item-8"><a href="https://tehnojam.ru/category/category/hardware">Железо</a> </li> <li class="cat-item cat-item-195"><a href="https://tehnojam.ru/category/category/zhiza">Жиза</a> </li> <li class="cat-item cat-item-10"><a href="https://tehnojam.ru/category/category/internet-of-things">Интернет вещей</a> </li> <li class="cat-item cat-item-9"><a href="https://tehnojam.ru/category/category/back-to-the-future">Назад в будущее</a> </li> <li class="cat-item cat-item-6"><a href="https://tehnojam.ru/category/category/it-news">Новости IT</a> </li> <li class="cat-item cat-item-5"><a href="https://tehnojam.ru/category/category/development">Разработка</a> </li> <li class="cat-item cat-item-4"><a href="https://tehnojam.ru/category/category/software">Софт</a> </li> <li class="cat-item cat-item-11"><a href="https://tehnojam.ru/category/category/itagregator">Техноагрегатор</a> </li> <li class="cat-item cat-item-7"><a href="https://tehnojam.ru/category/category/technologies">Технологии</a> </li> </ul> </aside><aside id="better_recent_comments-2" class="widget inner-padding widget_recent_comments"><h2 class="widget-title">Свежие комментарии</h2><ul id="better-recent-comments" class="recent-comments-list with-avatars"><li class="recentcomments recent-comment"><div class="comment-wrap" style="padding-left:50px; min-height:44px;"><span class="comment-avatar"><img alt='' src='https://secure.gravatar.com/avatar/63c47a5e6bc3a81cafd134836c7237be?s=40&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/63c47a5e6bc3a81cafd134836c7237be?s=80&d=mm&r=g 2x' class='avatar avatar-40 photo' height='40' width='40' loading='lazy' decoding='async'/></span> <span class="comment-author-link">Сандро</span> к записи <span class="comment-post"><a href="https://tehnojam.ru/category/software/category-software-kak-ubrat-razryvy-jekrana-tiring-v-linux.html#comment-4833">Избавляемся от тиринга в Linux</a></span></div></li><li class="recentcomments recent-comment"><div class="comment-wrap" style="padding-left:50px; min-height:44px;"><span class="comment-avatar"><img alt='' src='https://secure.gravatar.com/avatar/609176b69341b5e6a31037ca6d55644c?s=40&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/609176b69341b5e6a31037ca6d55644c?s=80&d=mm&r=g 2x' class='avatar avatar-40 photo' height='40' width='40' loading='lazy' decoding='async'/></span> <span class="comment-author-link">qqnq</span> к записи <span class="comment-post"><a href="https://tehnojam.ru/category/software/category-software-kak-ubrat-razryvy-jekrana-tiring-v-linux.html#comment-3840">Избавляемся от тиринга в Linux</a></span></div></li><li class="recentcomments recent-comment"><div class="comment-wrap" style="padding-left:50px; min-height:44px;"><span class="comment-avatar"><img alt='' src='https://secure.gravatar.com/avatar/46b75adcbec108fb5394dc238f5f54e2?s=40&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/46b75adcbec108fb5394dc238f5f54e2?s=80&d=mm&r=g 2x' class='avatar avatar-40 photo' height='40' width='40' loading='lazy' decoding='async'/></span> <span class="comment-author-link"><a href="https://www.comss.ru" class="url" rel="ugc external nofollow">Анатолий</a></span> к записи <span class="comment-post"><a href="https://tehnojam.ru/category/software/times-new-roman-linux.html#comment-3714">Как установить Times New Roman в Linux</a></span></div></li><li class="recentcomments recent-comment"><div class="comment-wrap" style="padding-left:50px; min-height:44px;"><span class="comment-avatar"><img alt='' src='https://secure.gravatar.com/avatar/5e0b5163198e3050a6cc8c6eff2be92d?s=40&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/5e0b5163198e3050a6cc8c6eff2be92d?s=80&d=mm&r=g 2x' class='avatar avatar-40 photo' height='40' width='40' loading='lazy' decoding='async'/></span> <span class="comment-author-link">Glass</span> к записи <span class="comment-post"><a href="https://tehnojam.ru/category/development/zashifrovannye-bazy-dannyh-sqlite-s-python-i-sqlcipher.html#comment-3632">Зашифрованные базы данных SQLite с Python и SQLCipher</a></span></div></li><li class="recentcomments recent-comment"><div class="comment-wrap" style="padding-left:50px; min-height:44px;"><span class="comment-avatar"><img alt='' src='https://secure.gravatar.com/avatar/3341d132c715c136b2be6e3951f89a99?s=40&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/3341d132c715c136b2be6e3951f89a99?s=80&d=mm&r=g 2x' class='avatar avatar-40 photo' height='40' width='40' loading='lazy' decoding='async'/></span> <span class="comment-author-link">Аир</span> к записи <span class="comment-post"><a href="https://tehnojam.ru/category/software/times-new-roman-linux.html#comment-3587">Как установить Times New Roman в Linux</a></span></div></li><li class="recentcomments recent-comment"><div class="comment-wrap" style="padding-left:50px; min-height:44px;"><span class="comment-avatar"><img alt='' src='https://secure.gravatar.com/avatar/915e7d080c998420e477852e89058857?s=40&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/915e7d080c998420e477852e89058857?s=80&d=mm&r=g 2x' class='avatar avatar-40 photo' height='40' width='40' loading='lazy' decoding='async'/></span> <span class="comment-author-link"><a href="http://Weexxx.com" class="url" rel="ugc external nofollow">dob4ig</a></span> к записи <span class="comment-post"><a href="https://tehnojam.ru/category/software/times-new-roman-linux.html#comment-3560">Как установить Times New Roman в Linux</a></span></div></li></ul></aside><aside id="recent-posts-widget-with-thumbnails-2" class="widget inner-padding recent-posts-widget-with-thumbnails"> <div id="rpwwt-recent-posts-widget-with-thumbnails-2" class="rpwwt-widget"> <h2 class="widget-title">Свежие записи</h2> <ul> <li><a href="https://tehnojam.ru/category/development/sozdanie-bezopasnogo-flutter-prilozhenija-ispolzuja-jwt.html"><img width="70" height="70" src="https://tehnojam.ru/wp-content/uploads/2024/09/snimok-jekrana_20240925_203152-1-150x150.png" class="attachment-70x70 size-70x70 wp-post-image" alt="" decoding="async" loading="lazy" /><span class="rpwwt-post-title">Создание безопасного Flutter приложения используя JWT</span></a></li> <li><a href="https://tehnojam.ru/category/development/5-osnovnyh-shablonov-proektirovanija-kotorye-dolzhen-znat-kazhdyj-flutter-inzhener.html"><img width="70" height="70" src="https://tehnojam.ru/wp-content/uploads/2024/09/snimok-jekrana-2024-09-10-220512-150x150.png" class="attachment-70x70 size-70x70 wp-post-image" alt="5 Essential Design Patterns Every Flutter Engineer Should Master" decoding="async" loading="lazy" /><span class="rpwwt-post-title">5 Основных шаблонов Проектирования, которые должен знать каждый Flutter инженер</span></a></li> <li><a href="https://tehnojam.ru/category/development/perehod-na-flutter-3-22-dlja-web.html"><img width="70" height="70" src="https://tehnojam.ru/wp-content/uploads/2024/09/flutterweb322-150x150.png" class="attachment-70x70 size-70x70 wp-post-image" alt="" decoding="async" loading="lazy" /><span class="rpwwt-post-title">Переход на Flutter 3.22 для Web</span></a></li> <li><a href="https://tehnojam.ru/category/development/kak-hranit-kljuchi-api-vo-flutter-dart-define-vs-env.html"><img width="70" height="70" src="https://tehnojam.ru/wp-content/uploads/2024/09/store_api_keys_flutter-150x150.png" class="attachment-70x70 size-70x70 wp-post-image" alt="" decoding="async" loading="lazy" /><span class="rpwwt-post-title">Как хранить ключи API во Flutter: —dart-define vs .env</span></a></li> <li><a href="https://tehnojam.ru/category/development/otlozhennaja-zagruzka-vo-flutter-preimushhestva-nedostatki-i-realizacija.html"><img width="70" height="70" src="https://tehnojam.ru/wp-content/uploads/2024/09/snimok-jekrana-2024-09-07-091140-150x150.png" class="attachment-70x70 size-70x70 wp-post-image" alt="" decoding="async" loading="lazy" /><span class="rpwwt-post-title">Отложенная загрузка во Flutter: преимущества, недостатки и реализация.</span></a></li> <li><a href="https://tehnojam.ru/category/development/kak-ispolzovat-flavor-vo-flutter-i-pochemu-jeto-tak-vazhno.html"><img width="70" height="70" src="https://tehnojam.ru/wp-content/uploads/2024/09/snimok-jekrana-2024-09-06-v-18.48.41-150x150.png" class="attachment-70x70 size-70x70 wp-post-image" alt="flautter flavors" decoding="async" loading="lazy" /><span class="rpwwt-post-title">Как использовать flavor во Flutter и почему это так важно</span></a></li> </ul> </div><!-- .rpwwt-widget --> </aside> </div><!-- .inside-right-sidebar --> </div><!-- #secondary --> </div><!-- #content --> </div><!-- #page --> <div class="site-footer grid-container grid-parent "> <footer class="site-info" itemtype="https://schema.org/WPFooter" itemscope="itemscope"> <div class="inside-site-info grid-container grid-parent"> <div class="copyright-bar"> © 2018 All rights reserved Tehnojam.pro </div> </div> </footer><!-- .site-info --> </div><!-- .site-footer --> <a title="Scroll back to top" rel="nofollow" href="#" class="koromo-back-to-top" style="opacity:0;visibility:hidden;" data-scroll-speed="400" data-start-scroll="300"> <span class="screen-reader-text">Scroll back to top</span> </a> <div class="koromo-side-left-cover"></div> <div class="koromo-side-right-cover"></div> </div> <link rel='stylesheet' id='hljstheme-css' href='https://tehnojam.ru/wp-content/plugins/wp-code-highlightjs/styles/default.css?ver=0.6.2' type='text/css' media='all' /> <script type="text/javascript" id="contact-form-7-js-extra"> /* <![CDATA[ */ var wpcf7 = {"apiSettings":{"root":"https:\/\/tehnojam.ru\/wp-json\/contact-form-7\/v1","namespace":"contact-form-7\/v1"}}; /* ]]> */ </script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/plugins/contact-form-7/includes/js/scripts.js?ver=5.1.9" id="contact-form-7-js"></script> <script type="text/javascript" id="pt-cv-content-views-script-js-extra"> /* <![CDATA[ */ var PT_CV_PUBLIC = {"_prefix":"pt-cv-","page_to_show":"5","_nonce":"0bfd64bf6f","is_admin":"","is_mobile":"","ajaxurl":"https:\/\/tehnojam.ru\/wp-admin\/admin-ajax.php","lang":"","loading_image_src":"data:image\/gif;base64,R0lGODlhDwAPALMPAMrKygwMDJOTkz09PZWVla+vr3p6euTk5M7OzuXl5TMzMwAAAJmZmWZmZszMzP\/\/\/yH\/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAPACwAAAAADwAPAAAEQvDJaZaZOIcV8iQK8VRX4iTYoAwZ4iCYoAjZ4RxejhVNoT+mRGP4cyF4Pp0N98sBGIBMEMOotl6YZ3S61Bmbkm4mAgAh+QQFCgAPACwAAAAADQANAAAENPDJSRSZeA418itN8QiK8BiLITVsFiyBBIoYqnoewAD4xPw9iY4XLGYSjkQR4UAUD45DLwIAIfkEBQoADwAsAAAAAA8ACQAABC\/wyVlamTi3nSdgwFNdhEJgTJoNyoB9ISYoQmdjiZPcj7EYCAeCF1gEDo4Dz2eIAAAh+QQFCgAPACwCAAAADQANAAAEM\/DJBxiYeLKdX3IJZT1FU0iIg2RNKx3OkZVnZ98ToRD4MyiDnkAh6BkNC0MvsAj0kMpHBAAh+QQFCgAPACwGAAAACQAPAAAEMDC59KpFDll73HkAA2wVY5KgiK5b0RRoI6MuzG6EQqCDMlSGheEhUAgqgUUAFRySIgAh+QQFCgAPACwCAAIADQANAAAEM\/DJKZNLND\/kkKaHc3xk+QAMYDKsiaqmZCxGVjSFFCxB1vwy2oOgIDxuucxAMTAJFAJNBAAh+QQFCgAPACwAAAYADwAJAAAEMNAs86q1yaWwwv2Ig0jUZx3OYa4XoRAfwADXoAwfo1+CIjyFRuEho60aSNYlOPxEAAAh+QQFCgAPACwAAAIADQANAAAENPA9s4y8+IUVcqaWJ4qEQozSoAzoIyhCK2NFU2SJk0hNnyEOhKR2AzAAj4Pj4GE4W0bkJQIAOw==","is_mobile_tablet":"","sf_no_post_found":"\u0417\u0430\u043f\u0438\u0441\u0435\u0439 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e."}; var PT_CV_PAGINATION = {"first":"\u00ab","prev":"\u2039","next":"\u203a","last":"\u00bb","goto_first":"\u041f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a \u043f\u0435\u0440\u0432\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435","goto_prev":"\u041f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435","goto_next":"\u041f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435","goto_last":"\u041f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435","current_page":"\u0422\u0435\u043a\u0443\u0449\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430","goto_page":"\u041f\u0435\u0440\u0435\u0439\u0442\u0438 \u043a \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435"}; /* ]]> */ </script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/plugins/content-views-query-and-display-post-page/public/assets/js/cv.js?ver=4.0.1" id="pt-cv-content-views-script-js"></script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/plugins/pt-content-views-pro/public/assets/js/cvpro.min.js?ver=5.4.1" id="pt-cv-public-pro-script-js"></script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/plugins/simple-share-buttons-adder/js/ssba.js?ver=6.6.2" id="simple-share-buttons-adder-ssba-js"></script> <script type="text/javascript" id="simple-share-buttons-adder-ssba-js-after"> /* <![CDATA[ */ Main.boot( [] ); /* ]]> */ </script> <!--[if lte IE 11]> <script type="text/javascript" src="https://tehnojam.ru/wp-content/themes/Koromo/js/classList.min.js?ver=1.0.1" id="koromo-classlist-js"></script> <![endif]--> <script type="text/javascript" src="https://tehnojam.ru/wp-content/themes/Koromo/js/menu.min.js?ver=1.0.1" id="koromo-menu-js"></script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/themes/Koromo/js/a11y.min.js?ver=1.0.1" id="koromo-a11y-js"></script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/themes/Koromo/js/navigation-search.min.js?ver=1.0.1" id="koromo-navigation-search-js"></script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/themes/Koromo/js/back-to-top.min.js?ver=1.0.1" id="koromo-back-to-top-js"></script> <script type="text/javascript" id="q2w3_fixed_widget-js-extra"> /* <![CDATA[ */ var q2w3_sidebar_options = [{"use_sticky_position":false,"margin_top":60,"margin_bottom":20,"stop_elements_selectors":"","screen_max_width":0,"screen_max_height":0,"widgets":[]}]; /* ]]> */ </script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/plugins/q2w3-fixed-widget/js/frontend.min.js?ver=6.2.3" id="q2w3_fixed_widget-js"></script> <script type="text/javascript" src="https://tehnojam.ru/wp-content/plugins/wp-code-highlightjs/highlight.common.pack.js?ver=0.6.2" id="hljs-js"></script> <style>code.hljs { /*margin: 5px;*/ }</style> <script type="text/javascript"> (function($, window) { var init_fn_flag = false; var init_fn = (function() { if (init_fn_flag) return; init_fn_flag = true; hljs.configure({"tabReplace":" "}); $('pre code').each(function(i, block) { hljs.highlightBlock(block); }); }); $(document).ready(init_fn); $(window).on("load", init_fn); })(jQuery, window); </script> </body> </html>