Skip to main content

No project description provided

Project description

Python In JavaScript

Причина появление этой штуковины обосновано тем, что в Python, по моему мнению, нет достойных фреймворков для создания десктоп приложений. Qt имеет лицензию GPL, а это значит что создавать коммерческие приложения на нем не законно, Tkinter стар, Kivy хорош, и лицензия MIT, но вам придется изучать Kv language, чтобы в нем разобраться, а найти товарища, который знает, или хочет изучать Kivy, будет тяжело. А вот HTML,CSS,JS сейчас преподают в начальных классах школы, поэтому товарищей по разработки, и готовой технологий много.

Мы(я) решили что Python хорош и удобен - во всем, но заточен для бекенде, а JavaScript приходится использовать на фронтенде, поэтому мы(я) сделали удобную интеграцию, под слоганом "Python в JavaScript".

Я бы хотел чтобы Python активно использовали для создания коммерческих десктоп приложения, чтобы было больше таких вакансий, чтобы Python программистам которые хотят создавать десктоп приложения, зарабатывали на любимом деле, и не мучились бы со всякими Delphi/Java потому что только такие, комические вакансии с низким уровнем входа, есть на рынке труда. Ричард Мэттью Столлман молодец, что агитирует за свободное ПО(не коммерческое), но быть доставщиком пиццы (так он советует программистам зарабатывать на жизнь) у мня плохо получается. Я не имею нечего против свободного ПО, я за свободное ПО, но когда свободные проекты запрещают(или делают платным) для использовать в коммерческих целях, это не свободное ПО -> это проприетарное ПО. Какая вредность от того, что твое ПО используют в коммерческих целях, что кто то обогащаются с помощью него ? В конечном итоге, это же помогает развитию бизнеса, что в следствие помогает развитию общества. Поэтому Pyjs свободен(бесплатен) для коммерческого(и не коммерческого) использования.

Python Выступает в качестве сервера, он по умолчанию асинхронный, и может одновременно обрабатывать десятки соединений, он может быть запущен как локально, так и удаленно. JavaScript отвечает за клиентский код, за визуал, вы можете использовать все доступные WEB инструменты для написания фронтенда, например сейчас есть поддержка Vue.js(Как пользоваться в Vue.js) .

  • Список фич:
    • Для быстрой и надежной интеграции, используются сетевой протокол WebSocket, и готовые стандартизированные JSON схемы(JSON Схема).
    • Со времени форматирования сообщения до момента получения результат от сервера, отклик - в среднем 0.002 секунды.
    • По умолчанию безопасность сервера обеспечивается обязательной аутентификацией по токену. Это имеет смысл с использованием шифрования TLS, но и без него это помогает избежать лишних подключений, например если у вас несколько приложений на pywjs и клиент случайно пытается подключиться не тому приложению.
    • Автоматическое пере подключение к серверу, при потере соединения с ним(На стороне JavaSctipt). Это невероятно полезная вещь при разработке программы на стороне сервера, когда его приходится постоянно пере запускать.
    • Для удобного и надежного создания десктоп приложений, есть несколько готовых вариантов отправки сообщений по протоколу WebSocket(Варианты отправки сообщения), например вы есть вариант send_dependent, в нем, ваше сообщение будет как обязательная зависимость для Python. Если потеряется связь с сервером, то при пере подключение к нему, ваши команды автоматически отправятся снова, это идеально подходит для динамического импорта(import_from_server) модулей на Python сервер . Или например транзакции send_transaction(Описание работы транзакций), в которых вы можете гарантированно отправлять сообщения, и получать на них ответ, а иначе, при возникновении множество возможных ошибок(например в течение 5 секунд, от сервера не пришел результат), обрабатывать их.

Интеграция

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:
                    {
                        alter(
                            JSON.stringify(
                                JSON.parse(response_obj.response),
                                null,
                                2
                            )
                        );
                    }
                    break;
            }
            /* wbs_obj.close(WbsCloseStatus.normal,'Пример Закрытия Соединения') */
        },
        // Функция обработка закрытия соединения с сервером
        callback_onclose: undefined,
        // Функция обработок ошибок при отправке на сервер
        callback_onerror: undefined,
        // Событие = Успешное подключение к серверу
        event_connect: undefined,
        // Событие = Не удалось подключиться к серверу
        event_error_connect: undefined,
        // Имя пользователя для этого клиента, используется в "кеш пользователя"
        user: "ИмяПользователя",
    });
    

Быстрый старт Vue.js

Итерация PyJS во vue.js происходит через хранилище(vuex). Все взаимодействие с Web Socket происходит в хранилище 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 инициализируем подключение к Web Socket. После этого можно отправлять сообщения.

      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({
        // system_response: -1,
        Алиас: Число_h_id,
    });
    
  2. Использование:

    // Получить h_id по алиасу -> (Используется для отправки и получения, сообщении на сервер)
    Алиасы.ids[Число_h_id];
    // Получить алиас по h_id  -> (Используется внутри клиентского приложения, для отладки, например для `PyjsLog`)
    Алиасы.names.Алиас;
    
Виджет для мониторинга подключения pyjs_log.vue

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

  • Подключить в App.vue

    <template>
        <PyjsLog v-model:isShow="isShow" :hids="hids" />
    </template>
    <script lang="ts">
        import PyjsLog from "@/components/pyjs_log.vue";
        import { Алиасы } from "./store/Хранилище";
        export default {
            components: { PyjsLog },
            data() {
                return {
                    isShow: true, // Показать или скрыть подробное окно
                    hids: Алиасы, // Алиасы для h_id. Об этом написаны в главе [[Использование алиасов для h_id]]
                };
            },
        };
    </script>
    

    pyjs_log

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

Варианты отправки сообщения
  • 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 },
                },
            });
        },
    });
    
Кто такой h_id ?

В Pywjs используется своеобразный способ получения сообщений. Так как по WebSoket у нас одно подключение(потому что это удобно), а ответом нужно получать много, используется вариант логического разделения ответа по h_id.

Например, клиент делает запрос в котором указывает h_id=99, сервер после обработки этого сообщения, вернет ответ с этим же h_id=99. Например, в реализации на Vue.js нужно использовать вычисляемые переменные, для реактивного реагирования, на ответ сервера, с конкретным h_id -> Быстрый старт Vue.js.

Каким бывает body ?

Атрибут body дает универсальность для запросов, которые используют различие Модификации запросов на сервер. По сути модификации - это как обработать сообщение, а 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 - Создать(или обновить) ключ который содержит пользовательский кеш.
    2. cache_read_key - Получить пользовательский кеш по указному ключу.

Рассмотрим каждую модификацию, в следующих главах.

Для того чтобы можно было посмотреть на ответ сервера, сделаем вот такой минимальный код. Или же воспользуйтесь -> Виджет для мониторинга подключения 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

Таблица 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
            // # Если не указан, то возьмется из конструктора  `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 wbs.wbs_server import wbs_main_loop
    from wbs.wbs_handle import WbsHandle
    from wbs.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 wbs.wbs_allowed_func import UserWbsFunc
from asyncio import create_subprocess_shell, subprocess


class МоиФункции(UserWbsFunc):

    # Асинхронная функция
    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
Функция в режиме транзакции

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

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

class МоиФункции(UserWbsFunc):

    @Transaction._(rollback=lambda: '!! Произошёл rollback на стороне сервера !!')
    async def readFile(path: str:
        """
        Прочесть файл
        """
        p = Path(path)
        if not p.exists():
            raise Transaction.TransactionError('Файл не существует.')
        else:
            with open(p, 'r') as f:
                return f.read()
const path = "/home/ФайлКоторогоНет";
wbs_obj.send_transaction(
    {
        mod: ClientsWbsRequest_Mod.func,
        h_id: 99,
        body: {
            n_func: "readFile", // Имя функцию которую вызвать
            args: [path], // Позиционные аргументы
        },
    },
    // Это функция `rollback`
    (
        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 пользователя.

События на сервере

"События сервера" - Функции которые выполняются в бесконечном цикле, и могут отправлять сообщение клиенту.

  • Пример отслеживания "События сервера" - переименование,создание,удаление файлов и директорий в указанном пути.

    import os
    from datetime import datetime
    from wbs.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 запросы, всё взаимодействие через готовые функции.

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

class UserWbsHandle(WbsHandle):
    path_user_cache= Path(__file__).parent / 'user_cache.sqlite'

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

Логирование на сервере

По умолчанию для логирование на сервере использует logsmal, её создал тот же автор, что и PywJs. Его основное предназночения для отладки и дебага программы, если вам нужно что то большее, то можете испоьзовать logging and loguru.

from wbs.wbs_logger import ABC_logger, defaultLogger

class UserWbsHandle(WbsHandle):
    # Определяем логер. По умолчанию используется https://pypi.org/project/logsmal/
    logger: ABC_logger = defaultLogger(path_to_dir_log=Path(__file__).parent)
Переопределние логгера по умолчанию

Если вам нужен другой логгер, то реализуйте абстрактный класс 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) = Структура запроса ClientsWbsRequest
  • Ответ от сервер(к Python) = Структура ответа ServerWbsResponse

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

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


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

  • ИмяПрограммы
    • client
      • index.html
    • server
      • main.py
      • requirements.py
    • auto_install.py
    • .gitignore

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

    1. Создать виртуальное окружение для Python.
    2. Установить зависемости из файла requirements.txt, в виртульно окружение.
    3. Создать файл auto_uninstall.py, для удаления программы.
    4. Создать файл auto_run.py, для запуска программы.
    5. Создать файл .gitignore
    6. (опцианально) открыть в браузере страницу, для продолжения установки программы -> Дополнительная установка программы.
  2. Шаг два - пользователь исполняет auto_run.py:

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

Дополнительная установка программы

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

TODO: Это не реализовано

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

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

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

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

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

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

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

  • Функция для получения текущего пути к html файлу. Это нужно для того, чтобы знать куда создавать символные файлы. Если открывать html файлы двойным кликом, то url будет равен абсолютному пути к этому html файлу - file://АбсольтныйПутьДля.html, поэтому можно узнать, куда динамически помещать символьные файлы, в зависемости от того, где запущен html файл.

    function getPath(): string {
        if (window.location.pathname == "/") {
            /*
            Если вы используете сервер для разрботчика на `vue`, то путь будет указываться не верно, он покажет url путь, а не путь к html файлу. Поэтому в этом слуаче укажем путь по умолчанию.
            */
            return "ПолныйПутьПоУмолчанию/raw";
        }
        /* Путь до папки с символьными файлами */
        let pathDirLinks_ = window.location.pathname
            .split(/[\/]/g)
            .slice(0, -1);
        let pathDirLinks = "";
        if (window.location.pathname.search(/\\/g) >= 0) {
            // Windows Файловая система
            pathDirLinks = pathDirLinks_.join("\\") + "\\raw";
        } else {
            // UNIX файловая система
            pathDirLinks = pathDirLinks_.join("/") + "/raw";
        }
        return pathDirLinks;
    }
    
  • Создать символьную ссылку на файл через Python

    def createLinkToFile(pathFile: str, pathDirLinks: str, extendsFile: Literal['txt', 'pdf', 'png', 'jpg', 'webp']) -> str:
        """Создать символьную ссылку на файл `pathFile`, и поместить ей в путь `pathDirLinks`
    
        :param pathFile: Путь к файлу на который нужно сделать символьную ссылку
        :param pathDirLinks: Путь к папке в которую нужно поместить символьную ссылку
        :param extendsFile: Какое разширение должно быть у символьного файла, это влият на то как браузер будет отображать этот файл.
        :return: Имя символьного файла
        """
    
        pathFile = Path(pathFile).resolve()
        pathDirLinks = Path(pathDirLinks).resolve()
        # Создаем путь если его нет
        if not os.path.exists(pathDirLinks):
            os.makedirs(pathDirLinks)
        # Имя ссылки = `link_ЗахешированныйПолныйПутьMD5__ИсходноеРасширениеФайла_.расширение`
        nameLinkFile: str = f"link_{hashlib.md5(str(pathFile).encode('utf-8')).hexdigest()}__{pathFile.suffix.lower().replace('.','_')}.{extendsFile}"
        absPathLink = pathDirLinks/nameLinkFile
    
        if absPathLink.exists():
            if not absPathLink.is_symlink():
                absPathLink.symlink_to(pathFile)
        else:
            absPathLink.symlink_to(pathFile)
        return str(absPathLink.name)
    
  • Использование в HTML (Это краткий, и условный пример)

    <!-- Если это Фото -->
    <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.3.tar.gz (66.8 kB view hashes)

Uploaded Source

Built Distribution

pywjs-0.0.3-py3-none-any.whl (56.0 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