Metadata-Version: 2.1
Name: pywjs
Version: 0.0.3
Summary: 
Home-page: https://github.com/denisxab/py_wjs
Author: Denis Vetkin
Requires-Python: >=3.11,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Requires-Dist: aiosqlite (>=0.17.0,<0.18.0)
Requires-Dist: jsonpickle (==2.2.0)
Requires-Dist: logsmal (==0.0.14)
Requires-Dist: pydantic (>=1.10.2,<2.0.0)
Requires-Dist: websockets (>=10.4,<11.0)
Project-URL: Repository, https://github.com/denisxab/py_wjs
Description-Content-Type: text/markdown

# 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](#Как-пользоваться-во-vuejs)) .

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

## Интеграция

### JavaScript

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

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

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

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

    ```html
    <script src="/js/wbs_type.js" defer></script>
    <script src="/js/wbs.js" defer></script>
    ```

2. Подробный пример шаблона для интеграции клиента на чистом `JavaScript`. Для безопасности сервера, подключения к нему осуществятся через токен. Вы можете придумать любой токен и вставить его вместо `ЛюбойТокенКоторыйВыРазрешилиНаСервере`.

    ```ts
    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`

        ```ts
        import { createStore } from "vuex";
        import { wbsStore } from "./wbsStore";

        export default createStore({
            modules: { wbs: wbsStore },
        });
        ```

    2. В компоненте `/src/App.vue` в методе `mounted` инициализируем подключение к `Web Socket`. После этого можно отправлять сообщения.

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

2. Отправляем сообщение на сервер.

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

        ```ts
        actions: {
            ЛюбоеИмя({dispatch}){
                dispatch(
                    "wbs/send",
                    {
                        mod: ClientsWbsRequest_Mod.exec,
                        h_id: 1,
                        body: {
                            exec = "2+2",
                        },
                    },
                    { root: true }
                );
            }
        }
        ```

    - Отправка сообщения из компонента:

        ```ts
        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`:

        ```ts
        getters: {
            ЛюбоеИмя(rootState) {
                const r=rootState.wbs.res.value[1]
                return r ? r : {};
            }
        }
        ```

    - Получить ответ в компоненте, для реактивности используем `computed`:

        ```ts
        computed: {
            ЛюбоеИмя() {
                const r=this.$store.state.wbs.res.value[1]
                return r ? r : {};
            }
        }
        ```

##### Использование алиасов для h_id

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

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

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

    ```ts
    import { ClassHID } from "wbs/wbs";
    export const Алиасы = new ClassHID({
        // system_response: -1,
        Алиас: Число_h_id,
    });
    ```

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

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

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

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

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

    ```html
    <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](_attachments/Pasted-image-20221031214211.png)

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

##### Варианты отправки сообщения

-   `wbs_obj.send({Запрос})` - Отправить сообщение на сервер. Это базовый вариант для отправки сообщений, все другие варианты используют этот, но добавляют некоторые особенности. **Описание**: Нет гарантий того, что вы получите на него ответ, если связь с сервером оборвется. И сообщение не будет ожидать подключения ! Если подключения нет, то сообщение пропадет, но целостность сообщения и порядок гарантирован. Наверное это слишком демотивирующие описание, того как работает протокол `TCP`.

    ```ts
    wbs_obj.send({
        mod: ClientsWbsRequest_Mod.exec,
        h_id: 99,
        // uid_c:  Автоматически сгенерируется
        body: {
            exec = "2+2",
        },
        // t_send: Автоматически сгенерируется
    });
    ```

    -   `mod` - [Модификации запросов на сервер](#Модификации-запросов-на-сервер).
    -   `h_id` - [Кто такой h_id ?](#Кто-такой-h_id-).
    -   `body` - [Каким бывает 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({Запрос},ЧтоВыполнитьЕслиОтветНеПолучен)` - Выполнить команду в режиме транзакции. [Описание работы транзакций](Без-названия-3.md#Описание-работы-транзакций). Сейчас транзакции поддерживаются только для модификаций запросов `func`. В `send_transaction` происходит подмена модификации `func` на её транзакционную модификацию `transaction_func`, поэтому в отправляемом `json` будет `mod:101` а не `mod:2`. **Главная причина** почему это нужно использовать - это обработка возможных ошибок, в функции `rollback`. Если в `send_force` все ошибки решаются обычной переотправкой, то в `send_transaction` вы можете осмысленно обрабатывать исключения. **Например** - вам нужно выполнить консольную команду на сервере, и вы бы не хотели, чтобы какая нибудь команда вдруг не выполнилась, и вы бы даже об этом не узнали. Вы бы могли придумать надежный вариант передачи команды через обычный `send`, но я уже это сделал за вас. При использовании `send_transaction` вы можете быть уверены - что команда, будет отправлена, выполнена, и вы получите успешный ответ, а иначе, все возможные ошибки будут переданы в функцию `rollback`, и в ней вы обработаете эти ошибки.

    ```ts
    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`, в которую передаст ответ `ПервогоЗапроса`. Это идеально подходит для получения "кеша пользователя", и последующего его использования в другом запросе на сервер. [--> **Использование кеша пользователя на стороне клиента**](#использование-кеша-пользователя-на-стороне-клиента)

    ```ts
    /* 
    Условный Пример: Когда пользователь закрывает страницу, мы записываем в пользовательский кеш, последний путь в кортом он был. Когда пользователь вновь откроет страницу, произойдет запрос с вариантом `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](#Быстрый-старт-vuejs).

##### Каким бывает 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`](#Виджет-для-мониторинга-подключения-`pyjs_log.vue`)

```ts
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

Выполнить произвольную команду. Сервер вернет значение переменной(или выражения) которая находится на последней строке запроса. Это больше сделано для фана, на практике лучше использовать доступные функции на сервере, например потому что их можно дебажить, и они логируются [-> Логирование на сервере](#Логирование-на-сервере)

```ts
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`.

```ts
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

Модификация для получения служебной информации о сервере.

```ts
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`

    ```ts
    wbs_obj.send({
        mod: ClientsWbsRequest_Mod.func,
        h_id: 83,
        body: {
            n_func: "getFileFromPath", // Имя функцию которую вызвать
            args: undefined, // Позиционные аргументы
            kwargs: { path: "/home" }, // Именованные аргументы
        },
    });
    ```

-   Вызвать асинхронную функцию `os_exe_async`(точно также как и синхронную)

    ```ts
    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` с указанной "Модификации события". Запущенное "Событие сервера" с указанной "Модификации события", доступно для отслеживания всем аутентифицированным клиентам. "События сервера" с указанной "Модификации события" автоматически остановиться если все клиенты от него отпишутся.

```ts
wbs_obj.send({
    mod: ClientsWbsRequest_Mod.event_create,
    h_id: 85,
    body: {
        n_func: "watchDir", // Имя функции которая отслеживает событие на сервере
        mod: "dubl", // Модификация событие
        args: ["/home"], // Позиционные аргументы
        kwargs: undefined, // Именованные аргументы
    },
});
```

###### event_sub

Про [-> События на сервере](#События-на-сервере)

**Подписаться** на оповещения срабатывания "события на севере"(с указанной модификацией). **Обратите внимание** создать и подписываться на события, можно из разных физических подключений(от разных клиентов), потому что "События сервера" с указанной "Модификации события" существует на сервере, и доступно всем аутентифицированным клиентам.

```ts
wbs_obj.send({
    mod: ClientsWbsRequest_Mod.event_sub,
    h_id: 86,
    body: {
        n_func: "watchDir", // Имя функции которая отслеживает событие на сервере
        mod: "dubl", // Модификация событие
    },
});
```

###### event_unsub

Про [-> События на сервере](#События-на-сервере)

**Отписаться** от оповещения срабатывания "события на севере"(с указанной модификацией). [-> event_create](#event_create)

```ts
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` - Создать ключ, который содержит "пользовательский кеш"

    ```ts
    wbs_obj.send({
        mod: ClientsWbsRequest_Mod.cache_add_key,
        h_id: 87,
        body: {
            key:"ИмяКлюча"
            value:ЗначениеКлюча // Это значение сериализуется в JSON
            // # Если не указан, то возьмется из конструктора  `new Wbs(user="ИмяПользователя")`, если такого пользователя не существет, то создастся новый.
            // user: "ИмяПользователя"
        }
    });
    ```

-   `cache_read_key` - Получить "пользовательский кеш" по указному ключу

    ```ts
    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`, в нем указываются все настройки.

    ```python
    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))
    ```

    -   `МоиФункции` [-> Доступные функции](#Доступные-функции)
    -   `МоиПодписки` [-> События на сервере](#События-на-сервере)
    -   `path_log` [-> Логирование на сервере](#Логирование-на-сервере)
    -   `path_user_cache` [-> Кеш пользователей](#Кеш-пользователей)

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

Можно использовать как **синхронные**, так и **асинхронные** функции. Для их вызова на стороне клиента используйте модификацию [-> func](#func).

Вот пример полезных функций.

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

```python
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()
```

```ts
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);
        }
    }
);
```

-   Шаблон транзакционной функции:

    ```python
    @Transaction._(rollback=ФункцияДляОтката)
    async def ИмяФункции(*args, **kwargs):
        ... # Что то делаем.
        if ... : # Что то пошло не так.
            raise Transaction.TransactionError('СообщениеДляКлиента') # Вызываем ошибку в транзакции.
        return ... # Если все хорошо, возвращаем ответ.
    ```

    -   `ФункцияДляОтката`(Можно не указывать) - Эта функция вызовется, если произошло любое не обработанное исключение. Результат этой функции будет передан клиенту, вмести с описанием ошибки.
    -   `СообщениеДляКлиента` - Как написано выше, `ФункцияДляОтката` - вызывается при любом не обработанном исключение, но лучше обрабатывать все исключения в этой же функции, для того чтобы логика обработки исключений не расползалась по всему проекту. Исключение `Transaction.TransactionError` нужно вызвать если это уже не решаемая проблема (например у клиента нет `root` доступа, и пока он его не передаст пароль, выполнение не может быть продолжено), то тогда, лучше обработать такое исключение в `ФункцияДляОтката`, и в нем же создать сообщения для клиента - чтобы он передал пароль от `root` пользователя.

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

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

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

    ```python
    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) # Отправка сообщения всем подписчикам для указанной модификации
    ```

    Шаблон отслеживания "события на сервере"

    ```python
    async def ИмяФункции(self_, path: str):
        ... # Инициализация локальных переменных
        while await self_.live(sleep=СколькоЖдать): # Бесконечный не блокирующий цикл событий, которые будет выполнятся через каждые `sleep`
        	... # Отслеживания события
        	if ... : # Условия срабатывания события
        		await self_.send(...)  # Отправка сообщения всем подписчикам для указанной модификации
    ```

#### Кеш пользователей

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

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

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

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

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

По умолчанию для логирование на сервере использует [logsmal](https://pypi.org/project/logsmal/), её создал тот же автор, что и `PywJs`. **Его основное предназночения для отладки и дебага программы**, если вам нужно что то большее, то можете испоьзовать `logging` and `loguru`.

```python
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](#Быстрый-старт-Python)

    ```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` файл.

    ```ts
    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`

    ```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 (Это краткий, и условный пример)

    ```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>
    ```

### Для гуру

Задач нет

