No project description provided
Project description
date created: 2022-11-22 23:15 date updated: 2022-11-22 23:20
Введение
Причина появление этой штуковины обосновано тем, что в Python
, по моему мнению, нет достойных фреймворков для создания коммерческих десктоп приложений. Qt
имеет лицензию GPL
, а это значит что создавать коммерческие приложения на нем не законно, Tkinter
стар, Kivy
хорош, и лицензия MIT
, но вам придется изучать Kv language
, чтобы в нем разобраться, а найти товарища, который знает, или хочет изучать Kivy
, будет тяжело, поэтому HR-ам будет туго. А вот HTML
,CSS
,JS
сейчас преподают в начальных классах школы, поэтому товарищей по разработки, и готовой технологий много.
Мне нужен был фреймворк:
- В котором можно использовать все библиотеки, и достижения в
Python
, и полный доступ к операционной системе(поэтомуwebassembly
не подходит). - Возможность использовать все библиотеки, и достижения WEB фронтенда.
Мы(я) решили что Python
хорош и удобен - во всем, но заточен для бекенде, а JavaScript
приходится использовать на фронтенде, поэтому мы(я) сделали удобную интеграцию, под слоганом "Python
в JavaScript
".
Я бы хотел чтобы
Python
активно использовали для создания коммерческих десктоп приложения, чтобы было больше таких вакансий, чтобыPython
программистам которые хотят создавать десктоп приложения, зарабатывали на любимом деле, и не мучились бы со всякимиDelphi
/Java
потому что только такие, комические вакансии с низким уровнем входа, есть на рынке труда. Ричард Мэттью Столлман молодец, что агитирует за свободное ПО(не коммерческое), но быть доставщиком пиццы (на одном из интервью, он так советует программистам зарабатывать на жизнь) у мня плохо получается. Я не имею нечего против свободного ПО, я за свободное ПО, но когда свободные ПО запрещают(или делают платным) для использовать в коммерческих целях, это не свободное ПО -> это проприетарное ПО. Какая вредность от того, что твое ПО используют в коммерческих целях, что кто то обогащаются с помощью него ? В конечном итоге, это же помогает развитию бизнеса, что в следствие помогает развитию общества. Если свободное ПО нельзя использовать в коммерческих целях, то тогда будут использовать проприетарное ПО в коммерческих целях... как говориться в "Притче о двух волка"- "Всегда побеждает тот волк, которого ты кормишь". ПоэтомуPywjs
свободен(бесплатен) для коммерческого(и не коммерческого) использования, у него даже нет лицензии и авторского права, поэтому если кото захочет украсть проект - я будут рад форку :)
Python
Выступает в качестве сервера, он по умолчанию асинхронный, и может одновременно обрабатывать десятки соединений, он может быть запущен как локально, так и удаленно. JavaScript
отвечает за клиентский код, за визуал, вы можете использовать все доступные WEB инструменты для написания фронтенда, например сейчас есть поддержка Vue.js
(Как пользоваться в Vue.js).
- Список фич:
- Для быстрой и надежной интеграции, используются сетевой протокол
WebSocket
, и готовые стандартизированныеJSON
схемы(JSON Схема), модифицированныйjson-rpc
. - Со времени форматирования сообщения до момента получения результат от сервера, отклик - в среднем
0.002
секунды. - По умолчанию безопасность сервера обеспечивается обязательной аутентификацией по токену. Это имеет смысл с использованием шифрования
TLS
, но и без него это помогает избежать лишних подключений, например если у вас несколько приложений наpywjs
и клиент случайно пытается подключиться не тому приложению. - Автоматическое переподключение к серверу, при потере соединения с ним(на стороне
JavaSctipt
). Это невероятно полезная вещь при разработке программы на стороне сервера, когда его приходится постоянно перезапускать. - Для удобного и надежного создания десктоп приложений, есть несколько готовых вариантов отправки сообщений по протоколу
WebSocket
(Варианты отправки сообщения), например вы есть вариантsend_dependent
, в нем, ваше сообщение будет как, обязательная зависимость дляPython
. Если потеряется связь с сервером, то при пере подключение к нему, ваши команды автоматически отправятся снова, это идеально подходит для динамического импорта(import_from_server) модулей наPython
сервер . Или например транзакцииsend_transaction
(Описание работы транзакций), в которых вы можете гарантированно отправлять сообщения, и получать на них ответ, а иначе, при возникновении множество возможных ошибок(например в течение 5 секунд, от сервера не пришел результат), обрабатывать их.
- Для быстрой и надежной интеграции, используются сетевой протокол
Связанные проекты с pywjs
:
- -> Корневой проект
py_wjs
и итеграция дляPython
- -> Клиентский проект
pyw_js
и интеграция дляJavaScript
- -> Тестовый стенд / Демо версия
- Примеры проектов на
pywj
:
Принципы pywjs
программы:
- Все необходимое должно быть их коробки, и поддерживаться официальным автором. Плагины это хорошо, но зоопарк плагинов это плохо.
- У пользователей не должно быть возможности сломать программу через стандартный функционал. А если он сломал программу через НЕ стандартный функционал, то попробовать восстановить целостность программы.
- Автономность программы - у каждого проекта свое виртуально окружение, и свой механизм обновления, это защищает от несовместимостей версий, которые могут возникнуть в глобальных ВО и механизмов обновления.
- Возможность использования сервера, как для десктопнго приложение, так и сетевого. С минимальными изменениями, в пользовательском коде фреймворка.
Интеграция
JavaScript
Быстрый старт JavaScript
Интеграция на стороне JavaScript
основана на протоколе WebSocket
. Браузер - клиент, Операционная система с Python
- сервер.
Как пользоваться в JavaScript
-
Для интеграции нужно импортировать в
HTML
файлы -wbs.js
(для логики) иwbs_type.js
(для типов)<script src="/js/wbs_type.js" defer></script> <script src="/js/wbs.js" defer></script>
-
Подробный пример шаблона для интеграции клиента на чистом
JavaScript
. Для безопасности сервера, подключения к нему осуществятся через токен. Вы можете придумать любой токен и вставить его вместоЛюбойТокенКоторыйВыРазрешилиНаСервере
.const wbs_obj = new Wbs("ЛюбойТокенКоторыйВыРазрешилиНаСервере", { // Хост host: "localhost", // Порт port: "9999", // Функция для отправки сообщений на сервер callback_onopen: () => { /* В этом примере мы отправляем запрос на сервер чтобы он посчитал 2+2 */ const command = "2+2"; wbs_obj.send({ mod: ClientsWbsRequest_Mod.exe, h_id: 99, uid_c: 0, body: { exec: command, }, }); }, // Функция для получения сообщений от сервера callback_onmessage: (event: MessageEvent) => { /* Здесь мы получем все ответы от сервера. Чтобы можно было по разному обрабатывать ответы от сервера есть атрибут `h_id`, которые мы передаем в запрос, и на который получаем здесь ответ. */ const response_obj = <ServerWbsResponse>JSON.parse(event.data); switch (response_obj.h_id) { case 99: { console.log(response_obj.response); } break; } /* wbs_obj.close(WbsCloseStatus.normal,'Пример Закрытия Соединения') */ }, // Функция обработка закрытия соединения с сервером callback_onclose: undefined, // Функция обработок ошибок при отправке на сервер callback_onerror: undefined, // Событие = Успешное подключение к серверу event_connect: undefined, // Событие = Не удалось подключиться к серверу event_error_connect: undefined, // Имя пользователя для этого клиента, используется в "кеш пользователя", по умолчанию user='base' user: "ИмяПользователя", });
Быстрый старт Vue.js
Итерация Pywjs
во vue.js
происходит через хранилище(vuex
). Всё взаимодействие с WebSocket
происходит в хранилище wbsStore.ts
Как пользоваться во Vue.js
-
Подключаем
wbsStore.ts
к проекту-
Подключаем хранилище как модуль:
/src/store/index.ts
import { createStore } from "vuex"; import { wbsStore } from "./wbsStore"; export default createStore({ modules: { wbs: wbsStore }, });
-
В компоненте
/src/App.vue
в методеmounted
инициализируем подключение кWebSocket
. После этого можно отправлять сообщения.beforeCreate() { this.$store.dispatch("wbs/initWebSocket", { // Что сделать после успешного подключения к серверу. after_connect: () => { // Тут отправляем первые сообщения на сервер. }, // Обработка события window.beforeunload(закрытия/перезагрузка страницы). Здесь можно выполнять отчистку ресурсов. destruction:()=>{} }); },
-
-
Отправляем сообщение на сервер.
-
Отправка сообщение из другого хранилища:
actions: { ЛюбоеИмя({dispatch}){ dispatch( "wbs/send", { // ПРИМЕР mod: ClientsWbsRequest_Mod.exec, h_id: 1, body: { exec = "2+2", }, }, { root: true } ); } }
-
Отправка сообщения из компонента:
methods: { ЛюбоеИмя(){ this.$store.dispatch("wbs/send", { // ПРИМЕР mod: ClientsWbsRequest_Mod.exec, h_id: 1, body: { exec = "2+2", }, }); } }
-
-
Получить ответ от сервера. В хранилище
wbsStore.ts
все ответы хранятся вstate.res.value
. Ключи уstate.res.value
будут равны той цифре которую вы указали в запросе в параметреh_id
. В примере выше мы указывалиh_id: 1
, поэтому получим ответ отvalue[1]
.-
Получить ответ в другом хранилище, для реактивности используем
getters
:getters: { ЛюбоеИмя(rootState) { const r=rootState.wbs.res.value[1] return r ? r : {}; } }
-
Получить ответ в компоненте, для реактивности используем
computed
:computed: { ЛюбоеИмя() { const r=this.$store.state.wbs.res.value[1] return r ? r : {}; } }
-
Использование алиасов для h_id
Цифры значат только величину, и не имеют другого смысла. Поэтому для понятного обозначения h_id
, разработчикам клиентской сотерны, рекомендую использовать алиасы.
-
Создать алиасы:
Например это файл
./store/Хранилище
import { ClassHID } from "wbs/wbs"; export const Алиасы = new ClassHID({ Алиас: Число_h_id, });
-
Использование:
// Получить имя алиаса по числу `h_id` Алиасы.ids[Число_h_id]; // Получить число `h_id` по имени алиаса Алиасы.names.Алиас;
Виджет для мониторинга подключения pyjs_log.vue
Готовый виджет для контроля подключения, и просмотра всех ответов от сервера.
- Показать/Скрыть окно.
- Статус подключения к серверу.
- Ответ сервера, на выбранный
h_id
- Список
h_id
на которые есть ответы. - Введите новый
URL
(дляWebSocket
) которому вам нужно подключиться. - Подключиться к новому
URL
(дляWebSocket
).
-
Подключить
pyjs_log
вApp.vue
<template> <PyjsLog :hids="hids" /> </template> <script lang="ts"> import PyjsLog from "@/components/pyjs_log.vue"; import { Алиасы } from "./store/Хранилище"; export default { components: { PyjsLog }, data() { return { // Алиасы для h_id. Об этом написаны в главе [-> Использование алиасов для h_id]] // Можно не указывать, тогда вместо алиасов будут `h_id.` hids: Алиасы, }; }, }; </script>
Тестовый стенд
Для того чтобы стразу попробовать программу на практике, воспользуйтесь тестовым стендом. В нём можно протестировать весь функционал pywjs
.
https://github.com/denisxab/pywjs_test_stand
Взаимодействие с сервером
Варианты отправки сообщения
-
wbs_obj.send({Запрос})
- Отправить сообщение на сервер. Это базовый вариант для отправки сообщений, все другие варианты используют этот, но добавляют некоторые особенности. Описание: Нет гарантий того, что вы получите на него ответ, если связь с сервером оборвется. И сообщение не будет ожидать подключения, прежде чем отправиться ! Если подключения нет, то сообщение пропадет, но целостность сообщения и порядок гарантирован. Наверное это слишком демотивирующие описание, того как работает протоколTCP
.wbs_obj.send({ mod: ClientsWbsRequest_Mod.exec, h_id: 99, // uid_c: Автоматически сгенерируется body: { exec = "2+2", }, // t_send: Автоматически сгенерируется });
mod
- Модификации запросов на сервер.h_id
- Кто такой h_id ?.body
- Каким бывает body ?.uid_c
- Логическое разделение сообщений, в пределах одногоh_id
, по умолчанию берется из генератораWbs.getUidC()
.t_send
- Время отправки сообщения(в UNIX). Используется для замера времени выполнения команды. По умолчанию, после получения сообщения от севера, формируется атрибутt_exec
который и указывает на время выполнения команды (в секундах).
-
wbs_obj.send_force({Запрос},ЗадержкаПереотправки)
- Принудительно отправить сообщение на сервер. Если для указаннойuid_c
в течениеЗадержкаПереотправки
не будет ответа от сервера, то это сообщение отправиться снова, это будет происходить пока мы не получим ответ для указаннойuid_c
. Сообщение НЕ будет, пере отправится, если во время ожидания ответа, связь с сервером была потеряна, он дождется восстановления подключения, и продолжит пере отправлять сообщение. Я называю это - "наглая транзакция", этот вариант отправки использует дляsend_dependent
. Вы, можете использоватьsend_force
, если любую возможную проблему, можно исправить обычной пере отправкой сообщения. Как говорят люди которые не первый день в программировании, 50% багов решаются обычной перезагрузкой/переотправкой. -
wbs_obj.send_dependent({Запрос},ЗадержкаПереотправки)
- Отправить команду, которая являются зависимостью для сервера. Если произошел обрыв связи с сервером, то при успешном пере подключение к нему, эти команды будут автоматически переотправлены. Отличие отsend_force
в том - чтоsend_force
, отправляет единожды сообщение, аsend_dependent
запоминает сообщение, и автоматически отправляет его, при каждом переподключение. Это полезна например для модификации запросаimport_from_server
, при таком методе отправки, вы можем быть уверены, что указные модули, будут обязательно импортированы на сервер, даже если он перезагрузился. -
wbs_obj.send_transaction({Запрос},ЧтоВыполнитьЕслиОтветНеПолучен)
- Выполнить команду в режиме транзакции. Описание работы транзакций. Сейчас транзакции поддерживаются только для модификаций запросовfunc
. Вsend_transaction
происходит подмена модификацииfunc
на её транзакционную модификациюtransaction_func
, поэтому в отправляемомjson
будетmod:101
а неmod:2
. Главная причина почему это нужно использовать - это обработка возможных ошибок, в функцииrollback
. Если вsend_force
все ошибки решаются обычной переотправкой, то вsend_transaction
вы можете осмысленно обрабатывать исключения. Например - вам нужно выполнить консольную команду на сервере, и вы бы не хотели, чтобы какая нибудь команда вдруг не выполнилась, и вы бы даже об этом не узнали. Вы бы могли придумать надежный вариант передачи команды через обычныйsend
, но я уже это сделал за вас. При использованииsend_transaction
вы можете быть уверены - что команда, будет отправлена, выполнена, и вы получите успешный ответ, а иначе, все возможные ошибки будут переданы в функциюrollback
, и в ней вы обработаете эти ошибки.wbs_obj.send_transaction( { mod: ClientsWbsRequest_Mod.func, h_id: 99, body: { n_func: "os_exe_async", // Имя функцию которую вызвать args: ["ls"], // Позиционные аргументы kwargs: undefined, // Именованные аргументы }, }, // Это функция `rollback` (error_code: TRollbackErrorCode, h_id: number, uid_c: number) => { alter("Rollback"); } );
-
wbs_obj.send_before({ПервыйЗапрос,before})
- Последовательная отправка сообщений. Выполнить отправкуПервогоЗапроса
на сервер, через вариантsend_force
, ожидать на него ответ, после получения успешного ответа, выполняет функциюbefore
, в которую передаст ответПервогоЗапроса
. Это идеально подходит для получения "кеша пользователя", и последующего его использования в другом запросе на сервер. -> Использование кеша пользователя на стороне клиента/* Условный Пример: Когда пользователь закрывает страницу, мы записываем в пользовательский кеш,последний путь в кортом он был. Когда пользователь вновь откроет страницу, произойдет запрос с вариантом `send_before` для получения из кеша последнего пути. После получения пути, делаем запрос на сервер для получения всех файлов. Таким образом можно сохранять состояние приложения на диске(для душнил=на запоминающем устройстве). */ wbs_obj.send_before({ // ПЕРВЫЙ ЗАПРОС mod: ClientsWbsRequest_Mod.cache_read_key, h_id: 87, body: { // Например: получаем путь к папке, в котрой пользователь был до закрытия страницы. key: "ПрошлыйПуть", }, // ВТОРОЙ ЗАПРОС before: (last_res: ServerWbsResponse) => { // Получаем прошлый путь из кеша const last_path = JSON.parse(last_res); wbs_obj.send({ mod: ClientsWbsRequest_Mod.func, h_id: 99, body: { // Например: Получим все файлы в указанной директории n_func: "ФункцияДляПолученияФайлов", kwargs: { path: last_path.response }, }, }); }, });
Кто такой h_id
В Pywjs
используется своеобразный способ получения сообщений. Так как по WebSoket
у нас одно подключение(потому что это удобно), а ответом нужно получать много, используется вариант логического разделения ответа по h_id
.
Например, клиент делает запрос в котором указывает h_id=99
, сервер после обработки этого сообщения, вернет ответ с этим же h_id=99
. Например, в реализации на Vue.js
нужно использовать вычисляемые переменные, для реактивного реагирования, на ответ сервера, с конкретным h_id
-> Быстрый старт Vue.js.
Каким бывает body
Атрибут body
дает универсальность для запросов, которые используют различие Модификации запросов на сервер. По сути модификации mod
- это как обработать сообщение, а body
- это само сообщение.
Все доступные варианты body
смотрите в ClientsWbsRequest.body
Описание работы транзакций
Как происходит передача сообщения в транзакции:
№ | Клиент | Сервер | |
---|---|---|---|
1 | Отправка сообщения на сервер. | -> | Сервер получает сообщение, |
2 | Клиент ожидает(указанное количество секунд) уведомления от сервера, о том что он принял сообщение. | <- | и отправляет об этом уведомление клиенту. |
3 | Клиент ожидает(указанное количество секунд) результат от сервера. Только если было принято уведомление | <- | Сервер выполняет команду, и отправляет результат |
Возможные ошибки в транзакции, и их обработка:
- На №1 = Сообщение не отправлено на сервер, тогда сработает
rollback
с кодомTRollbackErrorCode.timeout_notify
- превышено время ожидания уведомления. - На №2 = Сервер, по какой-либо причине, не уведомил клиента о получение сообщения, тогда сработает
rollback
с кодомTRollbackErrorCode.timeout_notify
- превышено время ожидания уведомления. - На №3 = Сервер уведомил клиента о получение сообщения, но по какой-либо причине не вернул результат команды, тогда сработает
rollback
с кодомTRollbackErrorCode.timeout_response
- превышено время ожидания результат. - На №3 = Во время выполнения команды, на сервере произошло не обработанное исключение, и он вызвал на своей стороне
rollback
, тогда сработает клиентскийrollback
с кодомTRollbackErrorCode.error_server
- откат по причине ошибки выполнения на сервера.
Модификации запросов на сервер
Все доступные модификации запросов хранятся в ClientsWbsRequest_Mod
.
-
Простые:
exec=3
- Выполнить произвольную команду на сервере.import_from_server=4
- Динамически импортировать указанные модули на сервер, только дляexec
(Выполнения произвольной команды).info=1
- Получить служебную информацию о сервере.
-
func=2
- Выполнить доступную функцию на сервере. Чаще всего вы будете использовать эту модификацию запроса.
-
event_create=5
- Запустить отслеживание события на сервере, и подписаться на него.event_sub=6
- Подписаться на событие сервера.event_unsub=7
- Отписаться от события сервера.
-
1-> Кеш пользователей 2-> Использование кеша пользователя на стороне клиента
cache_add_key=8
- Создать(или обновить) ключ, который содержит пользовательский кеш.cache_read_key=9
- Получить пользовательский кеш по указному ключу.
Рассмотрим каждую модификацию, в следующих главах.
Для того чтобы можно было посмотреть на ответ сервера, сделаем вот такой минимальный код. Или же воспользуйтесь -> Виджет для мониторинга подключения pyjs_log.vue
function main() {
// VVVV Вот тут пишем запросы для сервера VVVV
// //
// ^^^^ Вот тут пишем запросы для сервера ^^^^
}
const wbs_obj = new Wbs("ЛюбойТокенКоторыйВыРазрешилиНаСервере", {
host: "localhost",
port: 9999,
callback_onopen: main,
callback_onmessage: (event: MessageEvent) => {
// Выводим ответ сервера в консоль
console.log(<ServerWbsResponse>JSON.parse(event.data), null, 2);
},
});
exec
Выполнить произвольную команду. Сервер вернет значение переменной(или выражения) которая находится на последней строке запроса. Это больше сделано для фана, на практике лучше использовать доступные функции на сервере, например потому что их можно дебажить, и они логируются -> Логирование на сервере
const command = `
a=2
b=2
c=a+b
c
`;
wbs_obj.send({
mod: ClientsWbsRequest_Mod.exec,
h_id: 80,
body: {
exec: command, // Команда
},
});
import_from_server
Импортировать модули, они будут доступны для всех последующих модификаций exec
.
const command = `
import grp
import os
import pwd
import stat
from datetime import datetime
`;
wbs_obj.send({
mod: ClientsWbsRequest_Mod.import_from_server,
h_id: 81,
body: {
import_sts_exe: command, // Команда
},
});
info
Модификация для получения служебной информации о сервере.
wbs_obj.send({
mod: ClientsWbsRequest_Mod.info,
h_id: 82,
body: {
id_r: ClientsWbsRequest_GetInfoServer_id.help_allowed, // О чем информацию
text: undefined, // Дополнительные текст
},
});
-
id_r
- О чем информация:help_allowed
= Получить информацию(имя, аннотацию типов) о доступных функциях, которые вы можете выполнить на сервере. Структура ответа сервера описана вDT_HelpAllowed
. Доступные функции.info_event
= Получить информацию о подписках. События на сервере.check_token
= Выполняется автоматически при каждом подключение(переподключение) к серверу.
-
text
- Дополнительные текст. Например, используется для передачи токена.
func
Выполнить доступную функцию на сервере. Вот реализация на Python
сервере Доступные функции
-
Вызвать синхронную функцию
sum
wbs_obj.send({ mod: ClientsWbsRequest_Mod.func, h_id: 83, body: { n_func: "getFileFromPath", // Имя функцию которую вызвать args: undefined, // Позиционные аргументы kwargs: { path: "/home" }, // Именованные аргументы }, });
-
Вызвать асинхронную функцию
os_exe_async
(точно так же как и синхронную)wbs_obj.send({ mod: ClientsWbsRequest_Mod.func, h_id: 84, body: { n_func: "os_exe_async", // Имя функцию которую вызвать args: ["ls"], // Позиционные аргументы kwargs: undefined, // Именованные аргументы }, });
event_create
Создать отслеживание "события на севере", с указанной модификацией(mod
) ,и подписаться на него. "Модификации событий" - нужны чтобы одно и то же событие, можно было отслеживать с разными аргументами. "События сервера" по умолчанию отключены, их нужно запускать через event_create
с указанной "Модификации события". Запущенное "Событие сервера" с указанной "Модификации события", доступно для отслеживания всем аутентифицированным клиентам. "События сервера" с указанной "Модификации события" автоматически остановиться если все клиенты от него отпишутся.
wbs_obj.send({
mod: ClientsWbsRequest_Mod.event_create,
h_id: 85,
body: {
n_func: "watchDir", // Имя функции которая отслеживает событие на сервере
mod: "dubl", // Модификация событие
args: ["/home"], // Позиционные аргументы
kwargs: undefined, // Именованные аргументы
},
});
event_sub
Подписаться на оповещения срабатывания "события на севере"(с указанной модификацией). Обратите внимание создать и подписываться на события, можно из разных физических подключений(от разных клиентов), потому что "События сервера" с указанной "Модификации события" существует на сервере, и доступно всем аутентифицированным клиентам.
wbs_obj.send({
mod: ClientsWbsRequest_Mod.event_sub,
h_id: 86,
body: {
n_func: "watchDir", // Имя функции которая отслеживает событие на сервере
mod: "dubl", // Модификация событие
},
});
event_unsub
Отписаться от оповещения срабатывания "события на севере"(с указанной модификацией). -> event_create
wbs_obj.send({
mod: ClientsWbsRequest_Mod.event_sub,
h_id: 86,
body: {
n_func: "watchDir", // Имя функции которая отслеживает событие на сервере
mod: "dubl", // Модификация событие
},
});
Использование кеша пользователя на стороне клиента
Кеш пользователей храниться в реляционной БД, вот его структура.
Таблица main
user (unique PK) | key (unique PK) | idkey (unique) |
---|---|---|
Пользователь | Ключ в текстовом формате | ID на данные -> data.idkey |
По умолчанию будет создан пользователь по имени
base
Таблица data
idkey (unique PK) | json | hash |
---|---|---|
ID данных | Данные в формате JSON | Хеш данных столбца json в формате sha256 |
Вы можете увидеть, "пользовательский кеш", распределен по ключам. Это удобно, потому что предполагаю, вы будете хранить пользовательские настройки, и темы, а в них все распределено по ключ:значение
. Благодаря столбцу hash
можно не задумываться о проблеме лишних обновлениях столбца json
, он будет обновлен только если hash
в запросе отличается. Поэтому клиенту доступен только метод cache_add_key
которые и записывает и обновляет(если хеш разный) ключ.
-
cache_add_key
- Создать ключ, с "пользовательским кешем"wbs_obj.send({ mod: ClientsWbsRequest_Mod.cache_add_key, h_id: 87, body: { key:"ИмяКлюча" value:JSON.stringify(ЗначениеКлюча) // Если не указан, то возьмется из конструктора `new Wbs(user="ИмяПользователя")`, если такого пользователя не существет, то создастся новый. // user: "ИмяПользователя" } });
-
cache_read_key
- Получить "пользовательский кеш" по указному ключуwbs_obj.send({ mod: ClientsWbsRequest_Mod.cache_read_key, h_id: 87, body: { key: "ИмяКлюча", // Если не указан, то возьмется из конструктора `new Wbs(user="ИмяПользователя")`, если такого пользователя не существет, то будет ошибка. // user: "ИмяПользователя" }, });
Python
Быстрый старт Python
Интеграция на стороне Python
основана на WebSocket
. Через Python
мы сможем работать с операционной системой.
-
Настройка сервера - создаем файл
use_server.py
, в нем указываются все настройки.import asyncio from pathlib import Path from pywjs.wbs.server import wbs_main_loop from pywjs.wbs.handle import WbsHandle from pywjs.wbs.logger import ABC_logger, defaultLogger from Реализация import МоиФункции, МоиПодписки class UserWbsHandle(WbsHandle): # Класс с разрешенными функции (Обязательный) allowed_func = МоиФункции # Класс с "События на сервера" allowed_subscribe = МоиПодписки # Разрешенные токены для подключения allowed_token = set(['ЛюбойТокенКоторыйВыРазрешилиНаСервере']) # Путь для кеша пользователей (Обязательный) path_user_cache = Path(__file__).parent / 'user_cache.sqlite' # Определяем логер. По умолчанию используется https://pypi.org/project/logsmal/ (Обязательный) logger: ABC_logger = defaultLogger(path_to_dir_log=Path(__file__).parent) host = "localhost" port = 9999 if __name__ == '__main__': asyncio.run(wbs_main_loop(host, port, UserWbsHandle))
МоиФункции
-> Доступные функцииМоиПодписки
-> События на сервереpath_log
-> Логирование на сервереpath_user_cache
-> Кеш пользователей
Доступные функции
Можно использовать как синхронные, так и асинхронные функции. Для их вызова на стороне клиента используйте модификацию -> func.
Вот пример полезных функций.
import grp
import os
import pwd
import stat
from datetime import datetime
from pywjs.wbs.allowed_func import AllowWbsFunc
from asyncio import create_subprocess_shell, subprocess
class МоиФункции(AllowWbsFunc):
# Асинхронная функция
async def os_exe_async(command: str) -> dict:
"""
Выполнить асинхронно команды(command) в bash.
"""
# Выполняем команду
p = await create_subprocess_shell(
cmd=command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# Получить результат выполнения команды
stdout, stderr = await p.communicate()
return dict(
stdout=stdout.decode(),
stderr=stderr.decode(),
cod=p.returncode,
cmd=command,
)
# Синхронная функция
def getFileFromPath(path: str) -> str:
"""
Получить информацию о файлах и директориях по указанному пути `path`
"""
res = {}
for x in os.scandir(path):
tmp = {}
type_f = None
try:
d: os.stat_result = x.stat()
tmp['st_size'] = d.st_size # Размер в байтах
tmp['date_create'] = datetime.utcfromtimestamp(
int(d.st_ctime)).strftime('%Y-%m-%d %H:%M:%S') # Дата создания
tmp['date_update'] = datetime.utcfromtimestamp(
int(d.st_mtime)).strftime('%Y-%m-%d %H:%M:%S') # Дата изменения
tmp['user'] = pwd.getpwuid(d.st_uid).pw_name # Пользователь
tmp['group'] = grp.getgrgid(d.st_gid).gr_name # Группа
tmp['chmod'] = stat.S_IMODE(d.st_mode) # Доступ к файлу
except FileNotFoundError:
tmp['st_size'] = 0
tmp['date_create'] = 0
tmp['date_update'] = 0
tmp['user'] = 0
tmp['group'] = 0
tmp['chmod'] = 0
type_f = 'file'
if x.is_file():
type_f = 'file'
elif x.is_dir():
type_f = 'dir'
tmp['type_f'] = type_f
res[x.name] = tmp
return res
Пихать все функции в один класс не удобной, поэтому есть механизм для расширения доступных функций, путем множественного наследования. Это выглядит так:
from pywjs.wbs.allowed_func import AllowWbsFunc
class ДругойКласс_N:
def ИмяФункции():...
class ДругойКласс_1:
def ИмяФункции():...
class МоиФункции(AllowWbsFunc,ДругойКласс_1,ДругойКласс_N):
def ИмяФункции():...
Для того чтобы обратиться к функции из ДругойКласс_N
через модификатор func
- нужно указать имя класса, а потом через точку имя функции (ДругойКласс_N.ИмяФункции
). Для функций из класса МоиФункции
,не нужно указывать имя класса, пишите сразу ИмяФункции
.
Функция в режиме транзакции
Транзакционные функции хранятся также в AllowWbsFunc
, но они могут быть только асинхронные. Главная их цель в том, чтобы клиент смог обработать любое исключение которое может произойти, от момента отправки сообщения, далее, выполнения команды, и до момента получения ответа.
Пример простой функции, которую рационально использовать в транзакции. Например, нужно прочитать какой-нибудь указанный файл, но он может не существовать, в таком случае, нужно уведомить клиента, о том что, не возможно корректно выполнить эту команду. Клиент в свою очередь, в функции rollback
обрабатывает такую ситуацию, и уведомляет пользователя о том что такого файла нет.
Сервер:
from pywjs.wbs.allowed_func import Transaction, AllowWbsFunc
class МоиФункции(AllowWbsFunc):
@Transaction._(rollback=lambda: '!! Произошёл rollback на стороне сервера !!')
async def readFile(path: str:
"""
Прочесть файл
"""
p = Path(path)
if not p.exists():
raise Transaction.TransactionError('Файл не существует.')
else:
return p.read_text()
Клиент:
const path = "/home/ФайлКоторогоНет";
wbs_obj.send_transaction(
// Это основной запрос
{
mod: ClientsWbsRequest_Mod.func,
h_id: 99,
body: {
n_func: "readFile", // Имя функцию которую вызвать
args: [path], // Позиционные аргументы
},
},
// Это функция `rollback`
<TRollback>(
error_code: TRollbackErrorCode,
h_id: number,
uid_c: number,
res_server_json: ServerWbsResponse
) => {
alter(`Rollback: ошибка обработки файл "${path}"`);
if (error_code == TRollbackErrorCode.error_server) {
// Текст ошибки на сервере
alter(res_server_json.error);
}
}
);
-
Шаблон транзакционной функции:
@Transaction._(rollback=ФункцияДляОтката) async def ИмяФункции(*args, **kwargs): ... # Что то делаем. if ... : # Что то пошло не так. raise Transaction.TransactionError('СообщениеДляКлиента') # Вызываем ошибку в транзакции. return ... # Если все хорошо, возвращаем ответ.
ФункцияДляОтката
(Можно не указывать) - Эта функция вызовется, если произошло любое не обработанное исключение. Результат этой функции будет передан клиенту, вмести с описанием ошибки.СообщениеДляКлиента
- Как написано выше,ФункцияДляОтката
- вызывается при любом не обработанном исключение, но лучше обрабатывать все исключения в этой же функции, для того чтобы логика обработки исключений не расползалась по всему проекту. ИсключениеTransaction.TransactionError
нужно вызвать если это уже не решаемая проблема (например у клиента нетroot
доступа, и пока он его не передаст пароль, выполнение не может быть продолжено), то тогда, лучше обработать такое исключение вФункцияДляОтката
, и в нем же создать сообщения для клиента - чтобы он передал пароль отroot
пользователя.
Стандартные Доступные Функции
Для решения частных задач, с которыми вам придется столкнуться, при создании десктопные программ, существует сборники готовых "Доступных функций".
Вы можете подключать "Стандартные Доступные Функции"(StdAllowWbsFunc
) путем множественного наследования -> Используйте множественное наследование для расширения доступных функций. Вот пример:
from pywjs.wbs.allowed_func import AllowWbsFunc,StdAllowWbsFunc
class МоиФункции(AllowWbsFunc,StdAllowWbsFunc):
...
События на сервере
"События сервера" - Функции которые выполняются в бесконечном цикле, и могут отправлять сообщение клиенту.
-
Пример отслеживания "События сервера" - переименование,создание,удаление файлов и директорий в указанном пути.
import os from datetime import datetime from pywjs.wbs.subscribe import UserWbsSubscribe from asyncio import create_subprocess_shell, subprocess class МоиПодписки(UserWbsSubscribe): async def watchDir(self_, path: str): """ Отслеживание изменений файлов и директорий в пути `path` """ pre = [] # Инициализация локальных переменных while await self_.live(sleep=2): # Бесконечный не блокирующий цикл событий, которые будет выполнятся через каждые `sleep` f = os.listdir(path) # Отслеживания события if pre != f: # Условия срабатывания события pre = f await self_.send(f) # Отправка сообщения всем подписчикам для указанной модификации
Шаблон отслеживания "события на сервере"
async def ИмяФункции(self_, path: str): ... # Инициализация локальных переменных while await self_.live(sleep=СколькоЖдать): # Бесконечный не блокирующий цикл событий, которые будет выполнятся через каждые `sleep` ... # Отслеживания события if ... : # Условия срабатывания события await self_.send(...) # Отправка сообщения всем подписчикам для указанной модификации
Кеш пользователей
В большинстве десктоп приложений, у пользователей есть персональные данные, настройки, темы оформления. Все это является "кешем пользователя". Для легкой совместимостью с разными платформами, выбрано СУБД SQLite. Вам не нужно писать SQL запросы, всё взаимодействие через готовые функции.
Для того чтобы начать использовать "кеш пользователя" на сервере, нужно указать путь для БД которая будет его хранить.
class UserWbsHandle(WbsHandle):
path_user_cache= Path(__file__).parent / 'user_cache.sqlite'
На этом настройка кеширования на сервера заканчивается, спасибо за внимание. Как вы видите для использования кеширования пользователя, нужна одна строка кода. -> Использование кеша пользователя на стороне клиента
Часто бывают случаи когда нужно установить кеш по умолчанию, то есть записи должны быть добавлены в БД вмести с созданием таблицы. Для этого есть удобная функция init_user_cache
, в которой вы указываете записей в виде JSON
:
class UserWbsHandle(WbsHandle):
path_user_cache= Path(__file__).parent / 'user_cache.sqlite'
# Заполнить кеш пользователя значениями по умолчанию
def init_user_cache(self) -> dict:
return {
"ИмяПользователя": {
'Ключ': 'Значение'
}
}
Что еще нужно знать про пользовательский кеш:
- Базы данных с пользовательским кешем, автоматически восстанавливается, даже если её удалят во время работы сервера -> Если БД не существует(по пути
path_user_cache
) на момент вставки или чтения, то создаться новая БД, и выполнятся все настройки по умолчанию, а потом уже выполниться запрос, на вставку или чтения. Поэтому можно быть уверенным что БД всегда будет существовать в момент выполненияcache_add_key
/cache_read_key
- По умолчанию будет создан пользователь по имени
base
, это пользователь по умолчанию. - По умолчанию будет создан пользователь по имени
app
, в этом пользователи храниться глобальные настройки проекта. Ключи, которые начинаются на_
(нижнее подчеркивание) зарезервированы для системного пользования, и вы не можете их изменить через функциюcache_add_key
, вы можете только их прочитать через функциюcache_read_key
-> Структура пользователяapp
.(Пока нет ни каких зарезервированных для системы ключей, это ограничение - задел на бедующее)
Логирование на сервере
По умолчанию для логирование на сервере использует logsmal, её создал тот же автор, что и PywJs
. Его основное предназначения для отладки и дебага программы, если вам нужно что то большее, то можете использовать logging
and loguru
.
from pywjs.wbs.logger import ABC_logger, defaultLogger,EmptyLogger
class UserWbsHandle(WbsHandle):
# Определяем логер. По умолчанию используется https://pypi.org/project/logsmal/
logger: ABC_logger = defaultLogger(path_to_dir_log=Path(__file__).parent)
Eсли вам нужно отключить логирование(игнорировать все вызовы логера), то укажите logger=EmptyLogger
Переопределение логгера по умолчанию
Если вам нужен другой логгер, то реализуйте абстрактный класс wbs.wbs_logger.ABC_logger
В чем удовлетворительность логирования через logsmal
?
-
Использование логера в доступных функциях, и событиях на сервере. Настройка логера в главе -> Быстрый старт Python
# Импортируем переопределенный логер import wbs.wbs_server as baseWbs # Выполняем логирование baseWbs.logger.debug("Сообщение",['Флаг_1','Флаг_2','Флаг_N']) baseWbs.logger.info("Сообщение",['Флаг_1','Флаг_2','Флаг_N']) baseWbs.logger.success("Сообщение",['Флаг_1','Флаг_2','Флаг_N']) baseWbs.logger.warning("Сообщение",['Флаг_1','Флаг_2','Флаг_N']) baseWbs.logger.error("Сообщение",['Флаг_1','Флаг_2','Флаг_N'])
-
В чем плюсы:
- Лог сообщение в консоли обрезается, а полный текст записывается в файл. Это позволяет не засорять консоль огромными сообщениями.
- По умолчанию в
Linux
есть подсветка лог сообщений. - Полные и красивое описание исключений. Краткий текст исключений передастся в консоль с указанием
ERROR_LOG
, а полный текст, стек вызова, локальные переменен запишутся в файлdetail_error.log
, и поERROR_LOG
вы можете найти это исключение. - Автоматическое очистка лога файлов, при достижении размера
10mb
(можно указать не отчищать, а сжимать файл в архив). - В лог файл, строчки записываются в формате JSON. Вы можете без дополнительных преобразований, хранить их, например в
Elasticsearch
для аналитики.
JSON Схемы
- Отправка на сервер(к Python) = Структура запроса
pywjs.wbs.schema.ClientsWbsRequest
- Ответ от сервер(к Python) = Структура ответа
pywjs.wbs.schema.ServerWbsResponse
Установка PywJs программы
В обычном понимание, установка программа на PywJs
не нужна. Что html
файл, сразу готов к запуску, что python
(при наличии зависимых модулей) сразу готов запуску. Но все же, для пользователей которые, не знаю кто такой pip
, и как открывать консоль, нуждаются в некоторой автоматизации. Обязательно посмотрите на -> Требуемый шаблон для программы
-
Шаг раз - пользователь исполняет
auto_install.py
:- Создать виртуальное окружение для
Python
. - Установить зависимости из файла
server/pyproject.toml
, в виртуальное окружение. Используетсяpoetry
а не стандартныйpip
, потому чтоpoetry
позволяет синхронизировать зависимости, например, он удаляет модули которые установлены, но которых нетpyproject.toml
, это используется в -> Автообновлениеpywjs
программы - Создать файл
auto_uninstall.py
, для удаления программы. - Создать файл
auto_run.py
, для запуска программы. - Создать файл
.gitignore
, для игнорирования. - Создать файл
auto_update.py
, для автоматического обновления. -> Автообновлениеpywjs
программы
- Создать виртуальное окружение для
-
Шаг два - пользователь исполняет
auto_run.py
:- Запускается
index.html
в браузере по умолчанию. - Запускается
main.py
.
- Запускается
Требуемый шаблон для программы
ИмяПрограммы
client
- Для фронта- ...
index.html
- ...
server
- Для бека- ...
main.py
- Место для WbsHandlepyproject.toml
- Список зависимостей дляpython
, используемый вpoetry
- ...
auto_install.py
- Установка программы. Создать это файл, и скопировать код изhttps://github.com/denisxab/py_wjs/blob/main/builder/auto_install.py
- ...
Автообновление pywjs
программы
Пойдем "Быстрее, выше, сильнее" и дадим пользователям, которые так и не познакомились с pip
-ом и git
-ом, возможность автоматического обновления pywjs
программы. Но все же, пользователю придется сделать лишнее движение, это установить git
... Я бы рад написать свою независимую систему управления версий из коробки, и тогда бы пользователям не нужно было бы делать лишний шаг, но меня заклюют хейтеры, за то что я изобретаю велосипед, и в этом, я буду с ними согласен. Поэтому пользователю нужно объяснить как установить git.
Подразумевается что вы строго соблюдаете -> Требуемый шаблон для программы
Две вещи которые происходят в авто обновление программы:
- Синхронизация файлов и директорий через Git.д
- Синхронизация версией модулей в виртуальное окружение
python
, а также скачивание новых модулей, и удаление старых. Для этого используется библиотекаpoetry
, и в частности файлpyproject.toml
.
Проверка на необходимость обновления, происходит при выполнении auto_run.py
.
-
В этот момент выполняется команда
git diff
, если есть различия то выполняется синхронизация.git diff ЛокальнаяВетка origin/УдаленнаяВетка --raw
Из этого следует фича - даже если разработчик не вносил обновлений в проект, но у пользователя отличаются отслеживаемые файлы, то они перезапишутся на актуальные. Это своеобразное регенерация проекта(восстановление целостности), в том случае если пользователь случайно испортил исходный код программы, или уделил файл которые отслеживается в
git
. -
Если произошла синхронизация проекта, то тогда произойдет синхронизация зависимостей для виртуального окружения.
После выполнения обновления - запускается программа.
Особый подходы
Браузер это своя нравная программа, у него есть свои, не очевидные ограничения, которые нужно уметь обходить, для создания десктоп программ. Поэтому в этой главе будут показно как обходить эти ограничения.
Как открыть любой файл в браузере ?
По умолчанию, браузер запрещает доступ к файлам, которые находятся выше директории запущенного html
файла. Ему доступны файлы только на том же уровне вложенности, или ниже, но не выше ! Поэтому такое ограничение существует, и его нужно уметь обходить.
Самый оптимальный способ получать доступ к любому локальному файлу - это сделать символьную ссылку на нужный исходный файл, а символьный файл, поместить в директорию на том же уровне, что и html
файл, тогда браузер сможет самостоятельно открывать файлы, без использования сервера(почти). Сервер в этом случаи нужен только, для того чтобы сделать символьную ссылку, и поместить её в директорию с html
файлом.
Второй вариант, это читать файлы на стороне сервера, и передавать его по сети, но такой вариант более накладный, так как придется два раза сериализовать и десериализовать целый файл(или по частям), но все равно такой вариант мне нравиться, предполагается что клиент и сервер запущенны на одной машине, и поэтому нужно использовать все плюсы такой ситуации.
Пример готовых функций, для работы с символьными фалами:
-
getPath
- Функция для получения текущего пути кhtml
файлу. Это нужно для того, чтобы знать куда создавать символьные файлы. Если открыватьhtml
файлы двойным кликом, то url будет равен абсолютному пути к этомуhtml
файлу -file://АбсольтныйПутьДля.html
, поэтому можно узнать, куда динамически помещать символьные файлы, в зависимости от того, где запущенhtml
файл. Но если вы запускаетеhtml
через всякиеlive preview
или черезdev server
то тогда у вас не будет абсолютного пути к файлу, для такого случая нужно указать путь по умолчаниюgetPath({default_path:'ПутьПоУмолчанию'})
.import { getPath } from "wbs/wbs_std";
-
Создать символьную ссылку на файл через
Python
. Для этого используем стандартную доступную функциюStdAllowWbsFunc.createLinkToFile
. -> Тут написано как подключить стандартные доступные функцииwbs_obj.send({ mod: ClientsWbsRequest_Mod.func, h_id: 99, body: { n_func: "StdAllowWbsFunc.createLinkToFile", kwargs: { pathDirLinks: getPath(), // Путь к директоории в которой хранятся символьные ссылки pathFile: ... // Путь к файлу на который нужно сделать символьную ссылку extendsFile: ... // Разширение для файла } } });
-
Использование в HTML, это краткий, и условный пример. Вам нужно будет реализовать механизм получения пути на символьный файл, от функции
StdAllowWbsFunc.createLinkToFile
наvue.js
этот механизм уже реализован через вычисляемые значения.<!-- Если это Фото --> <img src="Путь.png" /> <!-- Если это PDF --> <iframe src="Путь.pdf" frameborder="0"></iframe> <!-- Если это Текстовый файл --> <div id="textDiv"></div> <script> /* Прочитать локальный ссылочный файл */ function readFile(pathLink) { fetch(pathLink) .then((response) => response.text()) .then((text) => { // @ts-ignore this.textFile = text; }); } textDiv = readFile("Путь.txt"); </script>
Для гуру
Задач нет
- Првоерить автообновление программы
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file pywjs-0.0.6.tar.gz
.
File metadata
- Download URL: pywjs-0.0.6.tar.gz
- Upload date:
- Size: 74.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.2.2 CPython/3.11.0rc1 Linux/5.15.0-53-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 03cbb08962137b498a3490e66da17fd9c47563f05ab4431d96e5d048a7d4ac29 |
|
MD5 | 45ec69107b9482abe02e6f510200ed2a |
|
BLAKE2b-256 | 9adbf772687eaf00cc280ee582ea60914d17023c4766f11bf7632dd1e99d45a2 |
File details
Details for the file pywjs-0.0.6-py3-none-any.whl
.
File metadata
- Download URL: pywjs-0.0.6-py3-none-any.whl
- Upload date:
- Size: 61.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.2.2 CPython/3.11.0rc1 Linux/5.15.0-53-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | bffba54653848b675257cd64ad441d71e953af532f0e6d2828116950325e1c1a |
|
MD5 | 0e410d47cf74a9cc7794e62a13989e9f |
|
BLAKE2b-256 | c78c180a351b286cf78daa27aa323187b39dd2045186363f1f58e546a92e96ee |