Open-source project that simplifies the creation of desktop applications based on Chromium
from .set_aplication import SetAplication
from .vars import Vars
from .get_aplication import GetAplication
pypulse/Aplication/__init__.py
zabbix-byte/PyPulse
class Aplication:
    instances = []
    
    def __init__(self, name: str) -> None:
        self.name = name

        Aplication.instances.append(self)

pypulse/Aplication/aplication.py
zabbix-byte/PyPulse
from .aplication import Aplication


class GetAplication:
    @staticmethod
    def primary() -> Aplication:
        for i in Aplication.instances:
            if i.primary:
                return i

pypulse/Aplication/get_aplication.py
zabbix-byte/PyPulse
import os
import ast

from .vars import Vars
from pypulse import View


def get_imports(node):
    imports = []
    for item in node.body:
        if isinstance(item, ast.Import):
            for alias in item.names:
                imports.append((alias.name, alias.asname))

        elif isinstance(item, ast.ImportFrom):
            module = item.module
            for alias in item.names:
                imports.append((f"{module}.{alias.name}", alias.asname))
    return imports


class ReadViews:
    @staticmethod
    def find_views_ho_are_using_the_decoratos(aplication_dir: str):

        complete_route_path = os.path.join(
            Vars.APLICATION_PATH, aplication_dir)

        dirs_module = []

        for path, subdirs, files in os.walk(complete_route_path):
            if '__pycache__' in path:
                continue
            for name in files:
                dirs_module.append(os.path.join(path, name))

        for file in dirs_module:
            with open(file, 'r') as file:
                file_contents = file.read()

            # Parse the file's content into an Abstract Syntax Tree (AST)
            parsed_ast = ast.parse(file_contents)

            target_decorator = "@view"

            all_imports = get_imports(parsed_ast)

            for node in ast.walk(parsed_ast):
                if isinstance(node, ast.FunctionDef):

                    for decorator in node.decorator_list:
                        if isinstance(decorator, ast.Call) and decorator.func.id == target_decorator.lstrip("@"):
                            current_name = None
                            current_path_trigger = None

                            for keyword in decorator.keywords:
                                if keyword.arg == "name":
                                    current_name = ast.dump(keyword.value).split(
                                        "value='")[-1].split("'")[0]
                                elif keyword.arg == "path_trigger":
                                    current_path_trigger = ast.dump(
                                        keyword.value).split("value='")[-1].split("'")[0]

                                if current_name is not None and current_path_trigger is not None:
                                    break

                            View.SetView(
                                f'{aplication_dir}___{current_name}',
                                node,
                                all_imports,
                                current_path_trigger
                            )

pypulse/Aplication/reader.py
zabbix-byte/PyPulse
from .aplication import Aplication
from .reader import ReadViews
from pypulse.View.view import View


class SetAplication:
    def __init__(self, name: str) -> None:
        # finding the views of this aplication
        self._check_if_aplication_is_already_registered(name)
        ReadViews.find_views_ho_are_using_the_decoratos(name)
        Aplication(name)

    def _check_if_aplication_is_already_registered(self, name:str):
        for i in Aplication.instances:
            if i.name == name:
                raise ValueError('The Aplication is already registered: {name}')
        
pypulse/Aplication/set_aplication.py
zabbix-byte/PyPulse
class Vars:
    APLICATION_PATH = None
    INTERNAL_HTTP_SERVER_PORT = 63333

pypulse/Aplication/vars.py
zabbix-byte/PyPulse
from .server import run_socket, stop_server
pypulse/Socket/__init__.py
zabbix-byte/PyPulse
import http.server

from pypulse import View
from pypulse.Template import Template
from pypulse.Utils import execute_ast_view_request
import urllib.parse

class RequestHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=Template.STATIC_PATH, **kwargs)

    def do_GET(self):
        current_view = View.CallView(self.path)
        if current_view.name is not None:
            request = {
                'method': 'GET',
                'headers': {
                    'Host': self.headers.get('Host'),
                    'Upgrade-Insecure-Requests': self.headers.get('Upgrade-Insecure-Requests'),
                    'User-Agent': self.headers.get('User-Agent'),
                    'Accept': self.headers.get('Accept'),
                    'Accept-Encoding': self.headers.get('Accept-Encoding'),
                    'Accept-Language': self.headers.get('Accept-Language')
                }
            }

            render = execute_ast_view_request(
                node_body=current_view.view, request=request, requirement_view=current_view.requirement_view, other_variables=current_view.variables_list)

            if type(render).__name__ == 'Redirect':
                render = render.get_render(request)

            template = str.join(" ", render.render_template().splitlines())
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(template.encode())
        else:
            super().do_GET()

    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_body = urllib.parse.parse_qs(self.rfile.read(content_length).decode('utf-8'))
        temp_post_body = {}

        for i in post_body:
            temp_post_body[i] = post_body[i][0]

        current_view = View.CallView(self.path)

        if current_view.name is not None:
            request = {
                'method': 'POST',
                'headers': {
                    'Host': self.headers.get('Host'),
                    'Upgrade-Insecure-Requests': self.headers.get('Upgrade-Insecure-Requests'),
                    'User-Agent': self.headers.get('User-Agent'),
                    'Accept': self.headers.get('Accept'),
                    'Accept-Encoding': self.headers.get('Accept-Encoding'),
                    'Accept-Language': self.headers.get('Accept-Language')
                },
                'body': temp_post_body
            }

            render = execute_ast_view_request(
                node_body=current_view.view, request=request, requirement_view=current_view.requirement_view)

            if type(render).__name__ == 'Redirect':
                render = render.get_render(request)

            template = str.join(" ", render.render_template().splitlines())
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(template.encode())
        else:
            super().do_POST()

    def do_PUT(self):
        content_length = int(self.headers['Content-Length'])
        post_body = self.rfile.read(content_length)
        temp_post_body = {}
        for i in post_body.decode('utf-8').split('&'):
            element = i.split('=')
            if len(element) == 2:
                temp_post_body[element[0]] = element[1]

        current_view = View.CallView(self.path)

        if current_view.name is not None:
            request = {
                'method': 'PUT',
                'headers': {
                    'Host': self.headers.get('Host'),
                    'Upgrade-Insecure-Requests': self.headers.get('Upgrade-Insecure-Requests'),
                    'User-Agent': self.headers.get('User-Agent'),
                    'Accept': self.headers.get('Accept'),
                    'Accept-Encoding': self.headers.get('Accept-Encoding'),
                    'Accept-Language': self.headers.get('Accept-Language')
                },
                'body': temp_post_body
            }

            render = execute_ast_view_request(
                node_body=current_view.view, request=request, requirement_view=current_view.requirement_view)

            if type(render).__name__ == 'Redirect':
                render = render.get_render(request)

            template = str.join(" ", render.render_template().splitlines())
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(template.encode())
        else:
            super().do_PUT()

    def do_PATCH(self):
        content_length = int(self.headers['Content-Length'])
        post_body = self.rfile.read(content_length)
        temp_post_body = {}
        for i in post_body.decode('utf-8').split('&'):
            element = i.split('=')
            if len(element) == 2:
                temp_post_body[element[0]] = element[1]

        current_view = View.CallView(self.path)

        if current_view.name is not None:
            request = {
                'method': 'PATCH',
                'headers': {
                    'Host': self.headers.get('Host'),
                    'Upgrade-Insecure-Requests': self.headers.get('Upgrade-Insecure-Requests'),
                    'User-Agent': self.headers.get('User-Agent'),
                    'Accept': self.headers.get('Accept'),
                    'Accept-Encoding': self.headers.get('Accept-Encoding'),
                    'Accept-Language': self.headers.get('Accept-Language')
                },
                'body': temp_post_body
            }

            render = execute_ast_view_request(
                node_body=current_view.view, request=request, requirement_view=current_view.requirement_view)

            if type(render).__name__ == 'Redirect':
                render = render.get_render(request)

            template = str.join(" ", render.render_template().splitlines())
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(template.encode())
        else:
            super().do_PATCH()

pypulse/Socket/request_handler.py
zabbix-byte/PyPulse
import socketserver
from .request_handler import RequestHandler
from pypulse.Aplication import Vars


httpd = socketserver.TCPServer(
    ("", Vars.INTERNAL_HTTP_SERVER_PORT), RequestHandler)


def run_socket():
    httpd.serve_forever()

def stop_server():
    httpd.shutdown()
    httpd.server_close()
pypulse/Socket/server.py
zabbix-byte/PyPulse
from .render_template import RenderTemplate
from .template import Template
from .redirect_path import Redirect

pypulse/Template/__init__.py
zabbix-byte/PyPulse
from pypulse.View import CallView
from pypulse.Utils import execute_ast_view_request


class Redirect():
    def __init__(self, path: str) -> None:
        self.view = CallView(path)

    def get_render(self, request):
        render = execute_ast_view_request(
            node_body=self.view.view, request=request, requirement_view=self.view.requirement_view)

        if type(render).__name__ == 'Redirect':
            raise RecursionError('You cant do a recursive redirect!')

        return render

pypulse/Template/redirect_path.py
zabbix-byte/PyPulse
from jinja2 import Environment, FileSystemLoader
from .template import Template


class RenderTemplate():
    def __init__(self, template_route: str, variables_dict: dict = None) -> None:
        self.template_route = template_route
        self.variables_dict = variables_dict

        self.envirenemnt = Environment(
            loader=FileSystemLoader(Template.TEMPLATE_PATH))

    def render_template(self):
        template = self.envirenemnt.get_template(self.template_route)
        if self.variables_dict is not None:
            output = template.render(self.variables_dict)
        else:
            output = template.render()
        return output

pypulse/Template/render_template.py
zabbix-byte/PyPulse
class Template:
    TEMPLATE_PATH: str = None
    STATIC_PATH: str = None
pypulse/Template/template.py
zabbix-byte/PyPulse
from .call_ast_exec_module import execute_ast_view_request
pypulse/Utils/__init__.py
zabbix-byte/PyPulse
import ast


def execute_ast_view_request(request: str, node_body: object, requirement_view: list, other_variables: dict = {}) -> None:
    module_body = []

    for i in requirement_view:
        import_q = i[0]
        import_as_name = i[1]

        if '.' in import_q:
            current_path = import_q.rsplit('.', 1)
            import_lib = ast.ImportFrom(
                module=current_path[0],
                names=[ast.alias(name=current_path[1], asname=import_as_name)],
                level=0
            )
        else:
            import_lib = ast.Import(
                names=[ast.alias(name=import_q, asname=import_as_name)])

        import_lib.lineno = 1
        import_lib.col_offset = 0
        module_body.append(import_lib)

    module_body.append(node_body)

    object_view = ast.Module(
        body=module_body, type_ignores=[])

    namespace = {}

    object_view = exec(
        compile(object_view, f'{node_body.name}_view', 'exec'),
        namespace
    )

    variables = {
        'request': request
    }

    variables.update(other_variables)

    return namespace[node_body.name](**variables)

pypulse/Utils/call_ast_exec_module.py
zabbix-byte/PyPulse
from .setview import view
from .setview import SetView
from .callview import CallView
pypulse/View/__init__.py
zabbix-byte/PyPulse
from .view import View


class CallView:
    def __init__(self, path_to_find: str) -> None:
        self.name = None
        self.view = None
        self.requirement_view = None
        self.path_trigger = None
        self.variables_list = {}

        path_to_find = path_to_find.split('/')

        for i in View.instances:
            path = i.path_trigger.split('/')

            if len(path) != len(path_to_find):
                continue

            path_contain_variable = False
            index = []
            variables_for_index = {}

            for j in path:
                if len(j) > 0:
                    path_variable_parameters = self.find_if_variable_in_path(j)
                    if path_variable_parameters[0] != False:
                        path_contain_variable = True

                        variables_for_index[j] = (
                            path_variable_parameters[2], path_variable_parameters[1])
                        index.append(j)

            # infind the variable and his context call
            if path_contain_variable:
                # this is the part wee dont check at moment
                view_finded = True

                for path_view in range(len(path)):
                    # excluding the variables
                    element_finded = False
                    for elements_in_path in index:
                        if path.index(elements_in_path) == path_view:
                            element_finded = True
                            break

                    if element_finded:
                        continue

                    if path[path_view] != path_to_find[path_view]:
                        view_finded = False

                if view_finded:
                    self.name = i.name
                    self.view = i.view
                    self.requirement_view = i.requirement_view
                    self.path_trigger = i.path_trigger

                    for current_index_path_variable in index:
                        self.variables_list[variables_for_index[current_index_path_variable][0]] = self.parse_varible(
                            path_to_find[path.index(current_index_path_variable)], variables_for_index[current_index_path_variable][1])

                    break

            if path_contain_variable == False:
                if path == path_to_find:
                    self.name = i.name
                    self.view = i.view
                    self.requirement_view = i.requirement_view
                    self.path_trigger = i.path_trigger
                    break

    def find_if_variable_in_path(self, current_path_element: str):
        # works whit: int, str, float

        methods_allowed = ['str', 'int', 'float']

        if '<' != current_path_element[0] and '>' != current_path_element[-1]:
            return (False, )

        if ':' not in current_path_element:
            raise ValueError(
                f'The parameter you are setting in the path {current_path_element} are incorrect formated')

        type_of_var, var_name = current_path_element[1:-1].split(':')

        if type_of_var.lower() not in methods_allowed:
            raise TypeError(
                f'The type you are passing in the path {current_path_element} are not in the list of allowed var types {methods_allowed}')

        return (True, type_of_var, var_name)

    def parse_varible(self, variable: str, type_var: str):
        if type_var.lower() == 'str':
            return str(variable)

        if type_var.lower() == 'int':
            return int(variable)

        if type_var.lower() == 'float':
            return float(variable)

pypulse/View/callview.py
zabbix-byte/PyPulse
from .view import View


class SetView:
    def __init__(self, name: str, view: object, requirement_view: list, path_trigger: str) -> None:
        self._validate_new_view_consistance(name, path_trigger)
        View(name, view, requirement_view, path_trigger)

    def _validate_new_view_consistance(self, name, path_trigger):
        if len(View.instances) == 0:
            return

        if name in [i.name for i in View.instances]:
            raise KeyError(f'The view with the name {name} already exists')

        if path_trigger in [i.path_trigger for i in View.instances]:
            raise KeyError(
                f'The view with the path_trigger {path_trigger} already exists')

        for i in View.instances:
            current_path_instance_new = []
            current_paths_in_views_new = []

            current_path_instance = path_trigger.split('/')
            current_paths_in_views = i.path_trigger.split('/')

            # removing the variables and see if match

            for j in current_path_instance:
                if len(j) == 0:
                    current_path_instance_new.append(j)
                    continue
                if j[0] != '<' and j[-1] != '>':
                    current_path_instance_new.append(j)

            for j in current_paths_in_views:
                if len(j) == 0:
                    current_paths_in_views_new.append(j)
                    continue
                if j[0] != '<' and j[-1] != '>':
                    current_paths_in_views_new.append(j)

            if current_path_instance_new == current_paths_in_views_new:
                raise KeyError(
                    f'The view with the path_trigger {path_trigger} already exists, coincidende path: {i.path_trigger}')


def view(name: str, path_trigger: str):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)  # Call the original function
            return result
        return wrapper
    return decorator

pypulse/View/setview.py
zabbix-byte/PyPulse
class View:
    instances = []

    def __init__(self, name: str, view: object,requirement_view:list, path_trigger: str) -> None:
        self.name = name
        self.view = view
        self.requirement_view = requirement_view
        self.path_trigger = path_trigger

        View.instances.append(self)

pypulse/View/view.py
zabbix-byte/PyPulse
from .load_browser import LoadBrowser
pypulse/Window/__init__.py
zabbix-byte/PyPulse
# Tested configurations:
# - wxPython 4.0 on Windows/Mac/Linux
# - wxPython 3.0 on Windows/Mac
# - wxPython 2.8 on Linux
# - CEF Python v66.0+

from pypulse.Aplication import Vars
import wx
from cefpython3 import cefpython as cef
import platform
import sys
import os

# Platforms
WINDOWS = (platform.system() == "Windows")
LINUX = (platform.system() == "Linux")
MAC = (platform.system() == "Darwin")

if MAC:
    try:
        # noinspection PyUnresolvedReferences
        from AppKit import NSApp
    except ImportError:
        print("[PyPulse] Error: PyObjC package is missing, "
              "cannot fix Issue #371")
        print("[PyPulse] To install PyObjC type: "
              "pip install -U pyobjc")
        sys.exit(1)

# Configuration
WIDTH = None
HEIGHT = None
WINDOW_TITLE = None
DEBUG = None
ICON = None
DEBUG_FILE = None
BOREDER_LESS = None

# Globals
g_count_windows = 0


def Browser(title: str,
            debug: bool,
            debug_file_name: str,
            window_size_x: int,
            window_size_y: int,
            icon_path: str,
            border_less: bool):

    global WIDTH, HEIGHT, WINDOW_TITLE, DEBUG, ICON, DEBUG_FILE, BOREDER_LESS

    WIDTH = window_size_x
    HEIGHT = window_size_y
    WINDOW_TITLE = title
    DEBUG = debug
    ICON = icon_path
    DEBUG_FILE = debug_file_name
    BOREDER_LESS = border_less

    check_versions()
    sys.excepthook = cef.ExceptHook  # To shutdown all CEF processes on error
    settings = {
        'product_version': 'PyPulse/10.00',
        'debug': DEBUG,
        'log_file': DEBUG_FILE,
    }

    if MAC:
        # Issue #442 requires enabling message pump on Mac
        # and calling message loop work in a timer both at
        # the same time. This is an incorrect approach
        # and only a temporary fix.
        settings["external_message_pump"] = True
    if WINDOWS:
        # noinspection PyUnresolvedReferences, PyArgumentList
        cef.DpiAware.EnableHighDpiSupport()

    cef.Initialize(settings=settings)
    app = CefApp(False)
    app.MainLoop()
    del app  # Must destroy before calling Shutdown
    if not MAC:
        # On Mac shutdown is called in OnClose
        cef.Shutdown()


def check_versions():
    print("[PyPulse] CEF Python {ver}".format(ver=cef.__version__))
    print("[PyPulse] Python {ver} {arch}".format(
        ver=platform.python_version(), arch=platform.architecture()[0]))
    print("[PyPulse] wxPython {ver}".format(ver=wx.version()))
    # CEF Python version requirement
    assert cef.__version__ >= "66.0", "CEF Python v66.0+ required to run this"


def scale_window_size_for_high_dpi(width, height):
    """Scale window size for high DPI devices. This func can be
    called on all operating systems, but scales only for Windows.
    If scaled value is bigger than the work area on the display
    then it will be reduced."""
    if not WINDOWS:
        return width, height
    (_, _, max_width, max_height) = wx.GetClientDisplayRect().Get()
    # noinspection PyUnresolvedReferences
    (width, height) = cef.DpiAware.Scale((width, height))
    if width > max_width:
        width = max_width
    if height > max_height:
        height = max_height
    return width, height


class MainFrame(wx.Frame):
    instance = None

    def __init__(self, parent, id=wx.ID_ANY, title="", pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE):
        super(MainFrame, self).__init__(parent, id, title, pos, size, style)
        Browser.instance = None
    
        # Must ignore X11 errors like 'BadWindow' and others by
        # installing X11 error handlers. This must be done after
        # wx was intialized.
        if LINUX:
            cef.WindowUtils.InstallX11ErrorHandlers()

        global g_count_windows
        g_count_windows += 1

        if WINDOWS:
            # noinspection PyUnresolvedReferences, PyArgumentList
            print("[PyPulse] System DPI settings: %s"
                  % str(cef.DpiAware.GetSystemDpi()))
        if hasattr(wx, "GetDisplayPPI"):
            print("[PyPulse] wx.GetDisplayPPI = %s" % wx.GetDisplayPPI())
        print("[PyPulse] wx.GetDisplaySize = %s" % wx.GetDisplaySize())

        print("[PyPulse] MainFrame declared size: %s"
              % str((WIDTH, HEIGHT)))
        size = scale_window_size_for_high_dpi(WIDTH, HEIGHT)
        print("[PyPulse] MainFrame DPI scaled size: %s" % str(size))

        wx.Frame.__init__(self, parent=None, id=wx.ID_ANY,
                          title=WINDOW_TITLE, size=size)
        # wxPython will set a smaller size when it is bigger
        # than desktop size.
        print("[PyPulse] MainFrame actual size: %s" % self.GetSize())

        self.setup_icon()
        self.Bind(wx.EVT_CLOSE, self.OnClose)

        # Set wx.WANTS_CHARS style for the keyboard to work.
        # This style also needs to be set for all parent controls.
        Browser.instance_panel = wx.Panel(self, style=wx.WANTS_CHARS)
        Browser.instance_panel.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
        Browser.instance_panel.Bind(wx.EVT_SIZE, self.OnSize)

        if MAC:
            # Make the content view for the window have a layer.
            # This will make all sub-views have layers. This is
            # necessary to ensure correct layer ordering of all
            # child views and their layers. This fixes Window
            # glitchiness during initial loading on Mac (Issue #371).
            NSApp.windows()[0].contentView().setWantsLayer_(True)

        if LINUX:
            # On Linux must show before embedding browser, so that handle
            # is available (Issue #347).
            self.Show()
            # In wxPython 3.0 and wxPython 4.0 on Linux handle is
            # still not yet available, so must delay embedding browser
            # (Issue #349).
            if wx.version().startswith("3.") or wx.version().startswith("4."):
                wx.CallLater(100, self.embed_browser)
            else:
                # This works fine in wxPython 2.8 on Linux
                self.embed_browser()
        else:
            self.embed_browser()
            self.Show()

    def setup_icon(self):
        self.SetIcon(wx.Icon(ICON))

    def embed_browser(self):
        window_info = cef.WindowInfo()
        (width, height) = Browser.instance_panel.GetClientSize().Get()
        assert Browser.instance_panel.GetHandle(), "Window handle not available"
        window_info.SetAsChild(Browser.instance_panel.GetHandle(),
                               [0, 0, width, height])
        Browser.instance = cef.CreateBrowserSync(
            window_info, url=f'http://127.0.0.1:{Vars.INTERNAL_HTTP_SERVER_PORT}/')
        Browser.instance.SetClientHandler(FocusHandler())

    def OnSetFocus(self, _):
        if not Browser.instance:
            return
        if WINDOWS:
            cef.WindowUtils.OnSetFocus(Browser.instance_panel.GetHandle(),
                                       0, 0, 0)
        Browser.instance.SetFocus(True)

    def OnSize(self, _):
        if not Browser.instance:
            return
        if WINDOWS:
            cef.WindowUtils.OnSize(Browser.instance_panel.GetHandle(),
                                   0, 0, 0)
        elif LINUX:
            (x, y) = (0, 0)
            (width, height) = Browser.instance_panel.GetSize().Get()
            Browser.instance.SetBounds(x, y, width, height)
        Browser.instance.NotifyMoveOrResizeStarted()

    def OnClose(self, event):
        print("[PyPulse] OnClose called")
        from pypulse.Socket import stop_server
        stop_server()
        if not Browser.instance:
            # May already be closing, may be called multiple times on Mac
            return

        if MAC:
            # On Mac things work differently, other steps are required
            Browser.instance.CloseBrowser()
            self.clear_browser_references()
            self.Destroy()
            global g_count_windows
            g_count_windows -= 1
            if g_count_windows == 0:
                cef.Shutdown()
                wx.GetApp().ExitMainLoop()
                # Call _exit otherwise app exits with code 255 (Issue #162).
                # noinspection PyProtectedMember
                os._exit(0)
        else:
            # Calling browser.CloseBrowser() and/or self.Destroy()
            # in OnClose may cause app crash on some paltforms in
            # some use cases, details in Issue #107.
            Browser.instance.ParentWindowWillClose()
            event.Skip()
            self.clear_browser_references()

    def clear_browser_references(self):
        # Clear browser references that you keep anywhere in your
        # code. All references must be cleared for CEF to shutdown cleanly.
        Browser.instance = None


class FocusHandler(object):
    def OnGotFocus(self, browser, **_):
        # Temporary fix for focus issues on Linux (Issue #284).
        if LINUX:
            print("[PyPulse] FocusHandler.OnGotFocus:"
                  " keyboard focus fix (Issue #284)")
            browser.SetFocus(True)


class CefApp(wx.App):

    def __init__(self, redirect):
        self.timer = None
        self.timer_id = 1
        self.is_initialized = False
        super(CefApp, self).__init__(redirect=redirect)

    def OnPreInit(self):
        super(CefApp, self).OnPreInit()
        # On Mac with wxPython 4.0 the OnInit() event never gets
        # called. Doing wx window creation in OnPreInit() seems to
        # resolve the problem (Issue #350).
        if MAC and wx.version().startswith("4."):
            print("[PyPulse] OnPreInit: initialize here"
                  " (wxPython 4.0 fix)")
            self.initialize()

    def OnInit(self):
        self.initialize()
        return True

    def initialize(self):
        if self.is_initialized:
            return
        self.is_initialized = True
        self.create_timer()
        frame = MainFrame(None)
        self.SetTopWindow(frame)
        frame.Show()

    def create_timer(self):
        # See also "Making a render loop":
        # http://wiki.wxwidgets.org/Making_a_render_loop
        # Another way would be to use EVT_IDLE in MainFrame.
        self.timer = wx.Timer(self, self.timer_id)
        self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
        self.timer.Start(10)  # 10ms timer

    def on_timer(self, _):
        cef.MessageLoopWork()

    def OnExit(self):
        self.timer.Stop()
        return 0

pypulse/Window/browser.py
zabbix-byte/PyPulse
import threading

from pypulse.Aplication import Vars
from pypulse.Socket import run_socket

from .browser import Browser, MainFrame


class LoadBrowser():
    def __init__(self, title: str = 'PyPulse App',
                 debug: bool = True,
                 debug_file_name: str = 'debug.log',
                 window_size_x: int = 1270,
                 window_size_y: int = 720,
                 icon_path: str = None,
                 border_less: bool = False
                 ) -> None:

        server_thread = threading.Thread(target=run_socket)
        server_thread.daemon = True
        server_thread.start()

        Browser(title, debug, debug_file_name, window_size_x, window_size_y, icon_path, border_less)

        while (not MainFrame.instance):
            pass

    @staticmethod
    def go_to_path(path: str):
        if MainFrame.instance:
            MainFrame.instance.GetMainFrame().LoadUrl(
                f'http://127.0.0.1:{Vars.INTERNAL_HTTP_SERVER_PORT}/{path}')

pypulse/Window/load_browser.py
zabbix-byte/PyPulse
import importlib.metadata

__all__ = ["window"]
__version__ = importlib.metadata.version('python-pulse')
__author__ = "The pypulse Authors"
pypulse/__init__.py
zabbix-byte/PyPulse
import argparse
import platform
import os
import importlib.metadata
import zipfile
import shutil
from pathlib import Path

MANAGE_lOGO = r"""
██████╗ ██╗   ██╗██████╗ ██╗   ██╗██╗     ███████╗███████╗
██╔══██╗╚██╗ ██╔╝██╔══██╗██║   ██║██║     ██╔════╝██╔════╝
██████╔╝ ╚████╔╝ ██████╔╝██║   ██║██║     ███████╗█████╗  
██╔═══╝   ╚██╔╝  ██╔═══╝ ██║   ██║██║     ╚════██║██╔══╝  
██║        ██║   ██║     ╚██████╔╝███████╗███████║███████╗
╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚══════╝╚══════╝╚══════╝                                                                                
"""

PATH_SEPARATOR = '/' if platform.system() == 'Linux' else '\\'


def main():
    print(f'v{importlib.metadata.version("python-pulse")}')
    print(MANAGE_lOGO)

    parser = argparse.ArgumentParser(
        description='PyPulse is a powerful tool designed to simplify the creation of desktop applications based on Chromium.\n Whether you\'re building custom web browsers, integrating web content into your application, or creating web-based desktop apps, PyPulse empowers you to do it efficiently and seamlessly.')
    parser.add_argument(
        '--create_project', help='This command initializes a new project in the PyPulse application, allowing you to specify the project name, template, and other configuration options.')
    parser.add_argument(
        '--create_app', help='The `create-app` command allows you to generate a new app within your PyPulse project.')

    args = parser.parse_args()

    if args.create_project:
        create_new_project(args.create_project)


def create_new_project(project_name_path: str):
    print('[PyPulse] Creating project....')

    if project_name_path is None:
        raise ValueError(
            '[PyPulse] create_new_project function only accept by parameter a string')

    if os.path.isdir(project_name_path):
        print('[PyPulse] Already exists a project with the same name in this path\n')
        exit()

    if os.path.isdir('tmp-project'):
        print('[PyPulse] Already exists a temp file in project\n')
        exit()

    os.mkdir('tmp-project')

    import requests
    project_name = project_name_path.split(PATH_SEPARATOR)[-1]

    response = requests.get(
        'https://github.com/zabbix-byte/PyPulse-HelloWorld-Project/archive/refs/heads/main.zip')
    f = open(os.path.join('tmp-project', 'temp.zip'), "wb")
    f.write(response.content)
    f.close()

    with zipfile.ZipFile(os.path.join('tmp-project', 'temp.zip'), 'r') as zip_ref:
        zip_ref.extractall('tmp-project')

    shutil.copytree(os.path.join(
        'tmp-project', 'PyPulse-HelloWorld-Project-main'), project_name_path)
    shutil.rmtree('tmp-project')

    ### Changing names
    path_to_project_old_name = os.path.join(project_name_path, 'HelloWorld.py')
    os.rename(path_to_project_old_name, os.path.join(project_name_path, f'{project_name_path}.py'))

    ### Repvomg files

    os.remove(os.path.join(project_name_path, 'README.md'))
    os.remove(os.path.join(project_name_path, '.gitignore'))


    print(f'[PyPulse] Project with name {project_name} has been created!\n')

pypulse/manage.py
zabbix-byte/PyPulse
from setuptools import find_packages, setup


# Read the contents of the README.md file
with open("README.md", "r", encoding="utf-8") as readme_file:
    long_description = readme_file.read()


setup(
    name='python-pulse',
    packages=find_packages(),
    version='0.1.6',
    description='Python, create desktop applications based on Chromium',
    long_description=long_description,
    long_description_content_type="text/markdown",
    author='zabbix-byte',
    license='MIT',
    install_requires=['cefpython3==66.1', 'Jinja2==3.1.2', 'wxPython==4.2.1'],
    classifiers=[
        # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package
        'Development Status :: 3 - Alpha',
        # Define that your audience are developers
        'Intended Audience :: Developers',
        'Topic :: Software Development :: Build Tools',
        'License :: OSI Approved :: MIT License',   # Again, pick a license
        'Programming Language :: Python :: 3.8',
    ],
    entry_points={
        'console_scripts': [
            'pypulse-manage = pypulse.manage:main'
        ]
    },
)

setup.py
zabbix-byte/PyPulse