Skip to main content

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 сейчас преподают в начальных классах школы, поэтому товарищей по разработки, и готовой технологий много.

Мне нужен был фреймворк:

  1. В котором можно использовать все библиотеки, и достижения в Python, и полный доступ к операционной системе(поэтому webassembly не подходит).
  2. Возможность использовать все библиотеки, и достижения 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:


Принципы pywjs программы:

  • Все необходимое должно быть их коробки, и поддерживаться официальным автором. Плагины это хорошо, но зоопарк плагинов это плохо.
  • У пользователей не должно быть возможности сломать программу через стандартный функционал. А если он сломал программу через НЕ стандартный функционал, то попробовать восстановить целостность программы.
  • Автономность программы - у каждого проекта свое виртуально окружение, и свой механизм обновления, это защищает от несовместимостей версий, которые могут возникнуть в глобальных ВО и механизмов обновления.
  • Возможность использования сервера, как для десктопнго приложение, так и сетевого. С минимальными изменениями, в пользовательском коде фреймворка.

Интеграция

JavaScript

Быстрый старт JavaScript

Интеграция на стороне JavaScript основана на протоколе WebSocket. Браузер - клиент, Операционная система с Python - сервер.

Как пользоваться в JavaScript

  1. Для интеграции нужно импортировать в HTML файлы - wbs.js(для логики) и wbs_type.js(для типов)

    <script src="/js/wbs_type.js" defer></script>
    <script src="/js/wbs.js" defer></script>
    
  2. Подробный пример шаблона для интеграции клиента на чистом 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

  1. Подключаем wbsStore.ts к проекту

    1. Подключаем хранилище как модуль: /src/store/index.ts

      import { createStore } from "vuex";
      import { wbsStore } from "./wbsStore";
      
      export default createStore({
        modules: { wbs: wbsStore },
      });
      
    2. В компоненте /src/App.vue в методе mounted инициализируем подключение к WebSocket. После этого можно отправлять сообщения.

      beforeCreate() {
        this.$store.dispatch("wbs/initWebSocket", {
            // Что сделать после успешного подключения к серверу.
            after_connect: () => {
                // Тут отправляем первые сообщения на сервер.
            },
            // Обработка события window.beforeunload(закрытия/перезагрузка страницы). Здесь можно выполнять отчистку ресурсов.
            destruction:()=>{}
        });
      },
      
  2. Отправляем сообщение на сервер.

    • Отправка сообщение из другого хранилища:

      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",
                 },
             });
         }
      }
      
  3. Получить ответ от сервера. В хранилище 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, разработчикам клиентской сотерны, рекомендую использовать алиасы.

  1. Создать алиасы:

    Например это файл ./store/Хранилище

    import { ClassHID } from "wbs/wbs";
    export const Алиасы = new ClassHID({
        Алиас: Число_h_id,
    });
    
  2. Использование:

    // Получить имя алиаса по числу `h_id`
    Алиасы.ids[Число_h_id];
    // Получить число `h_id` по имени алиаса   
    Алиасы.names.Алиас;
    

Виджет для мониторинга подключения pyjs_log.vue

Готовый виджет для контроля подключения, и просмотра всех ответов от сервера.

pyjs_log

  1. Показать/Скрыть окно.
  2. Статус подключения к серверу.
  3. Ответ сервера, на выбранный h_id
  4. Список h_id на которые есть ответы.
  5. Введите новый URL(для WebSocket) которому вам нужно подключиться.
  6. Подключиться к новому 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.

  • Простые:

    1. exec=3 - Выполнить произвольную команду на сервере.
    2. import_from_server=4 - Динамически импортировать указанные модули на сервер, только для exec(Выполнения произвольной команды).
    3. info=1 - Получить служебную информацию о сервере.
  • -> Доступные функции:

    1. func=2 - Выполнить доступную функцию на сервере. Чаще всего вы будете использовать эту модификацию запроса.
  • -> События на сервере

    1. event_create=5 - Запустить отслеживание события на сервере, и подписаться на него.
    2. event_sub=6 - Подписаться на событие сервера.
    3. event_unsub=7 - Отписаться от события сервера.
  • 1-> Кеш пользователей 2-> Использование кеша пользователя на стороне клиента

    1. cache_add_key=8 - Создать(или обновить) ключ, который содержит пользовательский кеш.
    2. 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))
    

Доступные функции

Можно использовать как синхронные, так и асинхронные функции. Для их вызова на стороне клиента используйте модификацию -> 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'])
    
  • В чем плюсы:

    1. Лог сообщение в консоли обрезается, а полный текст записывается в файл. Это позволяет не засорять консоль огромными сообщениями.
    2. По умолчанию в Linux есть подсветка лог сообщений.
    3. Полные и красивое описание исключений. Краткий текст исключений передастся в консоль с указанием ERROR_LOG, а полный текст, стек вызова, локальные переменен запишутся в файл detail_error.log, и по ERROR_LOG вы можете найти это исключение.
    4. Автоматическое очистка лога файлов, при достижении размера 10mb(можно указать не отчищать, а сжимать файл в архив).
    5. В лог файл, строчки записываются в формате JSON. Вы можете без дополнительных преобразований, хранить их, например в Elasticsearch для аналитики.

JSON Схемы

  • Отправка на сервер(к Python) = Структура запроса pywjs.wbs.schema.ClientsWbsRequest
  • Ответ от сервер(к Python) = Структура ответа pywjs.wbs.schema.ServerWbsResponse

Установка PywJs программы

В обычном понимание, установка программа на PywJs не нужна. Что html файл, сразу готов к запуску, что python(при наличии зависимых модулей) сразу готов запуску. Но все же, для пользователей которые, не знаю кто такой pip, и как открывать консоль, нуждаются в некоторой автоматизации. Обязательно посмотрите на -> Требуемый шаблон для программы

  1. Шаг раз - пользователь исполняет auto_install.py:

    1. Создать виртуальное окружение для Python.
    2. Установить зависимости из файла server/pyproject.toml, в виртуальное окружение. Используется poetry а не стандартный pip , потому что poetry позволяет синхронизировать зависимости, например, он удаляет модули которые установлены, но которых нет pyproject.toml, это используется в -> Автообновление pywjs программы
    3. Создать файл auto_uninstall.py, для удаления программы.
    4. Создать файл auto_run.py, для запуска программы.
    5. Создать файл .gitignore, для игнорирования.
    6. Создать файл auto_update.py, для автоматического обновления. -> Автообновление pywjs программы
  2. Шаг два - пользователь исполняет auto_run.py:

    1. Запускается index.html в браузере по умолчанию.
    2. Запускается main.py.

Требуемый шаблон для программы

  • ИмяПрограммы
    • client - Для фронта
      • ...
      • index.html
      • ...
    • server - Для бека
      • ...
      • main.py - Место для WbsHandle
      • pyproject.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.

  1. В этот момент выполняется команда git diff, если есть различия то выполняется синхронизация.

    git diff ЛокальнаяВетка origin/УдаленнаяВетка --raw
    

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

  2. Если произошла синхронизация проекта, то тогда произойдет синхронизация зависимостей для виртуального окружения.

После выполнения обновления - запускается программа.

Особый подходы

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

Как открыть любой файл в браузере ?

По умолчанию, браузер запрещает доступ к файлам, которые находятся выше директории запущенного 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

pywjs-0.0.6.tar.gz (74.7 kB view hashes)

Uploaded Source

Built Distribution

pywjs-0.0.6-py3-none-any.whl (61.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page